@lunora/bindings 1.0.0-alpha.2 → 1.0.0-alpha.4

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.
@@ -1,3 +1,4 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  /**
2
3
  * Structural types for the Analytics Engine write path.
3
4
  *
@@ -130,9 +131,8 @@ interface AnalyticsSqlResult {
130
131
  rowCount: number;
131
132
  rows: Record<string, unknown>[];
132
133
  }
133
- /** Thrown when the SQL API responds with a non-2xx status; carries the status + body for the caller to surface. */
134
- declare class AnalyticsSqlError extends Error {
135
- readonly status: number;
134
+ /** Thrown when the SQL API responds with a non-2xx status; a `LunoraError` subclass carrying the HTTP `status` + body for the caller to surface. */
135
+ declare class AnalyticsSqlError extends LunoraError {
136
136
  constructor(status: number, body: string);
137
137
  }
138
138
  /** The read client: a single `query(sql)` over the AE SQL API. */
@@ -1,3 +1,4 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  /**
2
3
  * Structural types for the Analytics Engine write path.
3
4
  *
@@ -130,9 +131,8 @@ interface AnalyticsSqlResult {
130
131
  rowCount: number;
131
132
  rows: Record<string, unknown>[];
132
133
  }
133
- /** Thrown when the SQL API responds with a non-2xx status; carries the status + body for the caller to surface. */
134
- declare class AnalyticsSqlError extends Error {
135
- readonly status: number;
134
+ /** Thrown when the SQL API responds with a non-2xx status; a `LunoraError` subclass carrying the HTTP `status` + body for the caller to surface. */
135
+ declare class AnalyticsSqlError extends LunoraError {
136
136
  constructor(status: number, body: string);
137
137
  }
138
138
  /** The read client: a single `query(sql)` over the AE SQL API. */
@@ -1,2 +1,2 @@
1
1
  export { createAnalytics } from '../packem_shared/createAnalytics-CEEI69o9.mjs';
2
- export { AnalyticsSqlError, createAnalyticsSqlClient } from '../packem_shared/AnalyticsSqlError-CGTdsi4H.mjs';
2
+ export { AnalyticsSqlError, createAnalyticsSqlClient } from '../packem_shared/AnalyticsSqlError-C2nz3jpH.mjs';
@@ -1,3 +1,3 @@
1
- export { createImages } from '../packem_shared/createImages-CJrvqX0u.mjs';
2
- export { buildImageDeliveryUrl } from '../packem_shared/buildImageDeliveryUrl-D1sVfIOP.mjs';
3
- export { buildSignedImageUrl, verifySignedImageUrl } from '../packem_shared/buildSignedImageUrl-Otdgc_jO.mjs';
1
+ export { createImages } from '../packem_shared/createImages-BzRnsz3H.mjs';
2
+ export { buildImageDeliveryUrl } from '../packem_shared/buildImageDeliveryUrl-B6_QU91n.mjs';
3
+ export { buildSignedImageUrl, verifySignedImageUrl } from '../packem_shared/buildSignedImageUrl-6iibA9v1.mjs';
@@ -174,4 +174,98 @@ interface Kv {
174
174
  */
175
175
  declare const scopeKey: (prefix: string, key: string) => string;
176
176
  declare const createKv: (options: LunoraKvOptions) => Kv;
177
- export { type KVNamespaceLike, type Kv, type KvGetOptions, type KvListKey, type KvListOptions, type KvListResult, type KvNamespaceListResult, type KvNamespacePutOptions, type KvPutOptions, type KvValue, type KvValueType, type KvValueWithMetadata, type LunoraKvOptions, createKv, scopeKey };
177
+ /** One KV namespace as the studio's KV browser surfaces it (mirrors the runtime's KvNamespaceSummary). */
178
+ interface KvNamespaceSummaryLike {
179
+ binding: string;
180
+ }
181
+ /** One key entry as surfaced by the KV admin browser (mirrors the runtime's KvKeyEntry). */
182
+ interface KvKeyEntryLike {
183
+ expiration?: number;
184
+ metadata?: unknown;
185
+ name: string;
186
+ }
187
+ /** A paginated page of KV keys (mirrors the runtime's KvKeyListResult). */
188
+ interface KvKeyListResultLike {
189
+ cursor?: string;
190
+ keys: KvKeyEntryLike[];
191
+ listComplete: boolean;
192
+ }
193
+ /** A value together with its metadata (mirrors the runtime's KvValueResult). */
194
+ interface KvValueResultLike {
195
+ metadata: unknown;
196
+ value: null | string;
197
+ }
198
+ /**
199
+ * The structural shape of a KV introspector — mirrors `KvIntrospector` from
200
+ * `@lunora/runtime` without importing it, keeping the dependency graph clean.
201
+ */
202
+ interface KvIntrospectorLike {
203
+ deleteKey: (options: {
204
+ key: string;
205
+ namespace: string;
206
+ }) => Promise<void>;
207
+ getValue: (options: {
208
+ key: string;
209
+ namespace: string;
210
+ }) => Promise<KvValueResultLike>;
211
+ listKeys: (options: {
212
+ cursor?: string;
213
+ limit?: number;
214
+ namespace: string;
215
+ prefix?: string;
216
+ }) => Promise<KvKeyListResultLike>;
217
+ listNamespaces: () => Promise<KvNamespaceSummaryLike[]>;
218
+ putValue: (options: {
219
+ expiration?: number;
220
+ expirationTtl?: number;
221
+ key: string;
222
+ metadata?: unknown;
223
+ namespace: string;
224
+ value: string;
225
+ }) => Promise<void>;
226
+ }
227
+ /** Construction options for {@link createKvIntrospector}. */
228
+ interface CreateKvIntrospectorOptions {
229
+ /**
230
+ * Map of binding name → bound KV namespace. Each entry becomes one
231
+ * namespace the studio can browse. Example:
232
+ * ```ts
233
+ * createKvIntrospector({ namespaces: { MY_KV: env.MY_KV } })
234
+ * ```
235
+ */
236
+ namespaces: Record<string, KVNamespaceLike>;
237
+ }
238
+ /**
239
+ * Build a `KvIntrospector`-compatible object from a map of bound KV namespaces.
240
+ * Pass the result as `kvIntrospector` on `WorkerOptions` to enable the studio's
241
+ * KV browser (`/_lunora/admin/kv/*` endpoints).
242
+ * @example
243
+ * ```ts
244
+ * createWorker({
245
+ * // …
246
+ * kvIntrospector: createKvIntrospector({ namespaces: { MY_KV: env.MY_KV } }),
247
+ * });
248
+ * ```
249
+ */
250
+ declare const createKvIntrospector: (options: CreateKvIntrospectorOptions) => KvIntrospectorLike;
251
+ /**
252
+ * Zero-config KV introspector: scan a worker `env` for every bound Workers KV
253
+ * namespace and register each under its binding name. Every `kv_namespaces` entry
254
+ * in `wrangler.jsonc` then appears in the studio's KV browser automatically — no
255
+ * hand-written `createKvIntrospector({ namespaces: … })` call, and any binding
256
+ * name (not just `KV`) and any number of namespaces light up. Non-KV bindings
257
+ * (R2, Durable Objects, queues, secrets, …) are skipped via {@link isKvNamespace}.
258
+ *
259
+ * Returns an introspector even when `env` holds no KV namespaces — its
260
+ * `listNamespaces()` resolves to `[]`, so the studio renders an empty state
261
+ * rather than a "not configured" error.
262
+ * @example
263
+ * ```ts
264
+ * createWorker({
265
+ * // …
266
+ * kvIntrospector: createKvIntrospectorFromEnv(env),
267
+ * });
268
+ * ```
269
+ */
270
+ declare const createKvIntrospectorFromEnv: (env: unknown) => KvIntrospectorLike;
271
+ export { type CreateKvIntrospectorOptions, type KVNamespaceLike, type Kv, type KvGetOptions, type KvIntrospectorLike, type KvListKey, type KvListOptions, type KvListResult, type KvNamespaceListResult, type KvNamespacePutOptions, type KvPutOptions, type KvValue, type KvValueType, type KvValueWithMetadata, type LunoraKvOptions, createKv, createKvIntrospector, createKvIntrospectorFromEnv, scopeKey };
@@ -174,4 +174,98 @@ interface Kv {
174
174
  */
175
175
  declare const scopeKey: (prefix: string, key: string) => string;
176
176
  declare const createKv: (options: LunoraKvOptions) => Kv;
177
- export { type KVNamespaceLike, type Kv, type KvGetOptions, type KvListKey, type KvListOptions, type KvListResult, type KvNamespaceListResult, type KvNamespacePutOptions, type KvPutOptions, type KvValue, type KvValueType, type KvValueWithMetadata, type LunoraKvOptions, createKv, scopeKey };
177
+ /** One KV namespace as the studio's KV browser surfaces it (mirrors the runtime's KvNamespaceSummary). */
178
+ interface KvNamespaceSummaryLike {
179
+ binding: string;
180
+ }
181
+ /** One key entry as surfaced by the KV admin browser (mirrors the runtime's KvKeyEntry). */
182
+ interface KvKeyEntryLike {
183
+ expiration?: number;
184
+ metadata?: unknown;
185
+ name: string;
186
+ }
187
+ /** A paginated page of KV keys (mirrors the runtime's KvKeyListResult). */
188
+ interface KvKeyListResultLike {
189
+ cursor?: string;
190
+ keys: KvKeyEntryLike[];
191
+ listComplete: boolean;
192
+ }
193
+ /** A value together with its metadata (mirrors the runtime's KvValueResult). */
194
+ interface KvValueResultLike {
195
+ metadata: unknown;
196
+ value: null | string;
197
+ }
198
+ /**
199
+ * The structural shape of a KV introspector — mirrors `KvIntrospector` from
200
+ * `@lunora/runtime` without importing it, keeping the dependency graph clean.
201
+ */
202
+ interface KvIntrospectorLike {
203
+ deleteKey: (options: {
204
+ key: string;
205
+ namespace: string;
206
+ }) => Promise<void>;
207
+ getValue: (options: {
208
+ key: string;
209
+ namespace: string;
210
+ }) => Promise<KvValueResultLike>;
211
+ listKeys: (options: {
212
+ cursor?: string;
213
+ limit?: number;
214
+ namespace: string;
215
+ prefix?: string;
216
+ }) => Promise<KvKeyListResultLike>;
217
+ listNamespaces: () => Promise<KvNamespaceSummaryLike[]>;
218
+ putValue: (options: {
219
+ expiration?: number;
220
+ expirationTtl?: number;
221
+ key: string;
222
+ metadata?: unknown;
223
+ namespace: string;
224
+ value: string;
225
+ }) => Promise<void>;
226
+ }
227
+ /** Construction options for {@link createKvIntrospector}. */
228
+ interface CreateKvIntrospectorOptions {
229
+ /**
230
+ * Map of binding name → bound KV namespace. Each entry becomes one
231
+ * namespace the studio can browse. Example:
232
+ * ```ts
233
+ * createKvIntrospector({ namespaces: { MY_KV: env.MY_KV } })
234
+ * ```
235
+ */
236
+ namespaces: Record<string, KVNamespaceLike>;
237
+ }
238
+ /**
239
+ * Build a `KvIntrospector`-compatible object from a map of bound KV namespaces.
240
+ * Pass the result as `kvIntrospector` on `WorkerOptions` to enable the studio's
241
+ * KV browser (`/_lunora/admin/kv/*` endpoints).
242
+ * @example
243
+ * ```ts
244
+ * createWorker({
245
+ * // …
246
+ * kvIntrospector: createKvIntrospector({ namespaces: { MY_KV: env.MY_KV } }),
247
+ * });
248
+ * ```
249
+ */
250
+ declare const createKvIntrospector: (options: CreateKvIntrospectorOptions) => KvIntrospectorLike;
251
+ /**
252
+ * Zero-config KV introspector: scan a worker `env` for every bound Workers KV
253
+ * namespace and register each under its binding name. Every `kv_namespaces` entry
254
+ * in `wrangler.jsonc` then appears in the studio's KV browser automatically — no
255
+ * hand-written `createKvIntrospector({ namespaces: … })` call, and any binding
256
+ * name (not just `KV`) and any number of namespaces light up. Non-KV bindings
257
+ * (R2, Durable Objects, queues, secrets, …) are skipped via {@link isKvNamespace}.
258
+ *
259
+ * Returns an introspector even when `env` holds no KV namespaces — its
260
+ * `listNamespaces()` resolves to `[]`, so the studio renders an empty state
261
+ * rather than a "not configured" error.
262
+ * @example
263
+ * ```ts
264
+ * createWorker({
265
+ * // …
266
+ * kvIntrospector: createKvIntrospectorFromEnv(env),
267
+ * });
268
+ * ```
269
+ */
270
+ declare const createKvIntrospectorFromEnv: (env: unknown) => KvIntrospectorLike;
271
+ export { type CreateKvIntrospectorOptions, type KVNamespaceLike, type Kv, type KvGetOptions, type KvIntrospectorLike, type KvListKey, type KvListOptions, type KvListResult, type KvNamespaceListResult, type KvNamespacePutOptions, type KvPutOptions, type KvValue, type KvValueType, type KvValueWithMetadata, type LunoraKvOptions, createKv, createKvIntrospector, createKvIntrospectorFromEnv, scopeKey };
package/dist/kv/index.mjs CHANGED
@@ -1 +1,2 @@
1
- export { createKv, scopeKey } from '../packem_shared/createKv-DTiSt216.mjs';
1
+ export { createKv, scopeKey } from '../packem_shared/createKv-7lNrS1W2.mjs';
2
+ export { createKvIntrospector, createKvIntrospectorFromEnv } from '../packem_shared/createKvIntrospector-CrAgQp1w.mjs';
@@ -1,10 +1,9 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const SQL_API_BASE = "https://api.cloudflare.com/client/v4/accounts";
2
- class AnalyticsSqlError extends Error {
3
- status;
4
+ class AnalyticsSqlError extends LunoraError {
4
5
  constructor(status, body) {
5
- super(`Analytics Engine SQL API returned ${String(status)}: ${body}`);
6
- this.name = "AnalyticsSqlError";
7
- this.status = status;
6
+ super("ANALYTICS_SQL_ERROR", `Analytics Engine SQL API returned ${String(status)}: ${body}`, { name: "AnalyticsSqlError", status });
8
7
  }
9
8
  }
10
9
  const createAnalyticsSqlClient = (config) => {
@@ -1,3 +1,4 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  import SelectBuilder from './SelectBuilder-DHaXZwn_.mjs';
2
3
  import { ident, toText } from './Sql-DceGtcUd.mjs';
3
4
 
@@ -10,12 +11,9 @@ const inferColumns = (rows) => {
10
11
  return { name };
11
12
  });
12
13
  };
13
- class R2SqlError extends Error {
14
- status;
14
+ class R2SqlError extends LunoraError {
15
15
  constructor(status, body) {
16
- super(`R2 SQL query failed (${String(status)}): ${body}`);
17
- this.name = "R2SqlError";
18
- this.status = status;
16
+ super("R2_SQL_ERROR", `R2 SQL query failed (${String(status)}): ${body}`, { name: "R2SqlError", status });
19
17
  }
20
18
  }
21
19
  const createR2Sql = (config) => {
@@ -1,10 +1,13 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const ABSOLUTE_URL_RE = /^[a-z][a-z\d+\-.]*:\/\//i;
2
4
  const stripTrailingSlash = (value) => value.endsWith("/") ? value.slice(0, -1) : value;
3
5
  const stripLeadingSlash = (value) => value.startsWith("/") ? value.slice(1) : value;
4
6
  const serializeTransform = (transform) => Object.entries(transform).filter(([, value]) => value !== void 0 && (typeof value === "string" || typeof value === "number")).map(([key, value]) => {
5
7
  const serialized = String(value);
6
8
  if (serialized.includes(",") || serialized.includes("=")) {
7
- throw new Error(
9
+ throw new LunoraError(
10
+ "INTERNAL",
8
11
  `@lunora/bindings/images: transform option \`${key}\` value \`${serialized}\` contains a \`,\` or \`=\`, which the /cdn-cgi/image/ option list cannot represent (these are the option/key-value separators). For colors, use the hex form (e.g. \`#RRGGBB\`/\`%23RRGGBB\`) instead of \`rgb(r,g,b)\`.`
9
12
  );
10
13
  }
@@ -18,7 +21,7 @@ const buildImageDeliveryUrl = (options) => {
18
21
  return `${base}/${encodeURIComponent(options.imageId)}/${encodeURIComponent(variant)}`;
19
22
  }
20
23
  if (options.key === void 0) {
21
- throw new Error("@lunora/bindings/images: buildImageDeliveryUrl requires either `imageId` or `key`");
24
+ throw new LunoraError("INTERNAL", "@lunora/bindings/images: buildImageDeliveryUrl requires either `imageId` or `key`");
22
25
  }
23
26
  const optionString = options.transform === void 0 ? "" : serializeTransform(options.transform);
24
27
  const isAbsolute = ABSOLUTE_URL_RE.test(options.key);
@@ -54,10 +54,10 @@ const encodeKey = (key) => key.split("/").map((segment) => encodeURIComponent(se
54
54
  const buildSignedImageUrl = async (options) => {
55
55
  const expiresInSeconds = options.expiresInSeconds ?? 60 * 60;
56
56
  if (!Number.isFinite(expiresInSeconds) || expiresInSeconds <= 0) {
57
- throw new Error("@lunora/bindings/images: expiresInSeconds must be a positive finite number");
57
+ throw new TypeError("@lunora/bindings/images: expiresInSeconds must be a positive finite number");
58
58
  }
59
59
  if (expiresInSeconds > MAX_EXPIRES_IN_SECONDS) {
60
- throw new Error(`@lunora/bindings/images: expiresInSeconds must not exceed ${String(MAX_EXPIRES_IN_SECONDS)} (7 days)`);
60
+ throw new TypeError(`@lunora/bindings/images: expiresInSeconds must not exceed ${String(MAX_EXPIRES_IN_SECONDS)} (7 days)`);
61
61
  }
62
62
  const exp = Math.floor(Date.now() / 1e3) + expiresInSeconds;
63
63
  const host = extractHost(options.baseUrl);
@@ -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 Error("@lunora/bindings/images: R2 object body is null (object missing or already consumed)");
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 Error("@lunora/bindings/images: width/height must be a positive finite number");
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 Error(`@lunora/bindings/images: unsupported output format "${format}" (allowed: ${[...ALLOWED_OUTPUT_FORMATS].join(", ")})`);
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,21 @@
1
+ import { LunoraError } from '@lunora/errors';
2
+
1
3
  const MAX_KEY_LENGTH = 512;
2
4
  const MAX_LIST_LIMIT = 1e3;
3
5
  const validateKey = (key) => {
4
6
  if (typeof key !== "string" || key.length === 0) {
5
- throw new Error("@lunora/bindings/kv: key must be a non-empty string");
7
+ throw new TypeError("@lunora/bindings/kv: key must be a non-empty string");
6
8
  }
7
9
  if (key.length > MAX_KEY_LENGTH) {
8
- throw new Error(`@lunora/bindings/kv: key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
10
+ throw new LunoraError("INTERNAL", `@lunora/bindings/kv: key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
9
11
  }
10
12
  if (key.includes("\0")) {
11
- throw new Error("@lunora/bindings/kv: key contains NUL byte");
13
+ throw new LunoraError("INTERNAL", "@lunora/bindings/kv: key contains NUL byte");
12
14
  }
13
15
  const segments = key.split("/");
14
16
  for (const segment of segments) {
15
17
  if (segment === "." || segment === "..") {
16
- throw new Error("@lunora/bindings/kv: key contains a `.`/`..` path component");
18
+ throw new LunoraError("INTERNAL", "@lunora/bindings/kv: key contains a `.`/`..` path component");
17
19
  }
18
20
  }
19
21
  };
@@ -22,15 +24,15 @@ const validatePrefix = (prefix) => {
22
24
  return;
23
25
  }
24
26
  if (prefix.length > MAX_KEY_LENGTH) {
25
- throw new Error(`@lunora/bindings/kv: prefix exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
27
+ throw new LunoraError("INTERNAL", `@lunora/bindings/kv: prefix exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
26
28
  }
27
29
  if (prefix.includes("\0")) {
28
- throw new Error("@lunora/bindings/kv: prefix contains NUL byte");
30
+ throw new LunoraError("INTERNAL", "@lunora/bindings/kv: prefix contains NUL byte");
29
31
  }
30
32
  const segments = prefix.split("/");
31
33
  for (const segment of segments) {
32
34
  if (segment === "." || segment === "..") {
33
- throw new Error("@lunora/bindings/kv: prefix contains a `.`/`..` path component");
35
+ throw new LunoraError("INTERNAL", "@lunora/bindings/kv: prefix contains a `.`/`..` path component");
34
36
  }
35
37
  }
36
38
  };
@@ -40,14 +42,14 @@ const scopeKey = (prefix, key) => {
40
42
  const trimmedPrefix = prefix.endsWith("/") ? prefix.slice(0, -1) : prefix;
41
43
  const composed = `${trimmedPrefix}/${key}`;
42
44
  if (composed.length > MAX_KEY_LENGTH) {
43
- throw new Error(`@lunora/bindings/kv: scoped key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
45
+ throw new LunoraError("INTERNAL", `@lunora/bindings/kv: scoped key exceeds ${String(MAX_KEY_LENGTH)}-byte limit`);
44
46
  }
45
47
  return composed;
46
48
  };
47
49
  const toPutOptions = (options) => {
48
50
  const out = {};
49
51
  if (options.expiration !== void 0 && options.expirationTtl !== void 0) {
50
- throw new Error("@lunora/bindings/kv: `expiration` and `expirationTtl` are mutually exclusive");
52
+ throw new LunoraError("INTERNAL", "@lunora/bindings/kv: `expiration` and `expirationTtl` are mutually exclusive");
51
53
  }
52
54
  if (options.expiration !== void 0) {
53
55
  out.expiration = options.expiration;
@@ -62,7 +64,7 @@ const toPutOptions = (options) => {
62
64
  };
63
65
  const createKv = (options) => {
64
66
  if (!options.namespace) {
65
- throw new Error("@lunora/bindings/kv: `namespace` is required");
67
+ throw new TypeError("@lunora/bindings/kv: `namespace` is required");
66
68
  }
67
69
  const { keyPrefix, namespace } = options;
68
70
  if (keyPrefix !== void 0) {
@@ -103,7 +105,7 @@ const createKv = (options) => {
103
105
  };
104
106
  const list = async (listOptions = {}) => {
105
107
  if (listOptions.limit !== void 0 && (!Number.isInteger(listOptions.limit) || listOptions.limit <= 0)) {
106
- throw new Error("@lunora/bindings/kv: `limit` must be a positive integer");
108
+ throw new TypeError("@lunora/bindings/kv: `limit` must be a positive integer");
107
109
  }
108
110
  let { prefix } = listOptions;
109
111
  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 (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) => {
@@ -28,11 +30,11 @@ const createVectorAdminIntrospector = (options) => {
28
30
  const queryIndex = async ({ name, text, topK }) => {
29
31
  const binding = indexes[name];
30
32
  if (binding === void 0) {
31
- throw new Error(`@lunora/bindings/vectors: no Vectorize binding registered for index "${name}"`);
33
+ throw new LunoraError("INTERNAL", `@lunora/bindings/vectors: no Vectorize binding registered for index "${name}"`);
32
34
  }
33
35
  const embed = embedders[name];
34
36
  if (embed === void 0) {
35
- throw new Error(`@lunora/bindings/vectors: no embedder registered for index "${name}" — it lists read-only`);
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 { LunoraError } from '@lunora/errors';
1
2
  import { c as concurrentMap, U as UPSERT_EMBED_CONCURRENCY } from './concurrent-Dj5sOibv.mjs';
2
3
 
3
4
  const resolveIndex = (indexes, name) => {
4
5
  const index = indexes[name];
5
6
  if (!index) {
6
- throw new Error(`@lunora/bindings/vectors: no index registered for "${name}". Known indexes: ${Object.keys(indexes).join(", ") || "(none)"}`);
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 Error("@lunora/bindings/vectors: at least one index binding is required");
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 Error("@lunora/bindings/vectors: query requires either `vector` or both `input` and `embed`");
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 Error(`@lunora/bindings/vectors: binding for "${indexName}" does not implement describe()`);
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,3 +1,4 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  /**
2
3
  * A composed SQL fragment. Carries the finished `text`; `toString()` returns it
3
4
  * so a fragment can be dropped straight into a template or `String(...)`.
@@ -265,8 +266,7 @@ declare class SelectBuilder<Row = Record<string, unknown>> implements Queryable<
265
266
  * envelope, or an unparseable body; carries the HTTP `status` and the raw body
266
267
  * for the caller to surface.
267
268
  */
268
- declare class R2SqlError extends Error {
269
- readonly status: number;
269
+ declare class R2SqlError extends LunoraError {
270
270
  constructor(status: number, body: string);
271
271
  }
272
272
  /**
@@ -1,3 +1,4 @@
1
+ import { LunoraError } from '@lunora/errors';
1
2
  /**
2
3
  * A composed SQL fragment. Carries the finished `text`; `toString()` returns it
3
4
  * so a fragment can be dropped straight into a template or `String(...)`.
@@ -265,8 +266,7 @@ declare class SelectBuilder<Row = Record<string, unknown>> implements Queryable<
265
266
  * envelope, or an unparseable body; carries the HTTP `status` and the raw body
266
267
  * for the caller to surface.
267
268
  */
268
- declare class R2SqlError extends Error {
269
- readonly status: number;
269
+ declare class R2SqlError extends LunoraError {
270
270
  constructor(status: number, body: string);
271
271
  }
272
272
  /**
@@ -1,5 +1,5 @@
1
1
  export { default as SelectBuilder } from '../packem_shared/SelectBuilder-DHaXZwn_.mjs';
2
- export { R2SqlError, createR2Sql } from '../packem_shared/R2SqlError-DlDd_SrE.mjs';
2
+ export { R2SqlError, createR2Sql } from '../packem_shared/R2SqlError-Boygg3S0.mjs';
3
3
  export { asc, desc, renderOrderTerm } from '../packem_shared/asc-Cur-xO8v.mjs';
4
4
  export { default as SetOperation } from '../packem_shared/SetOperation-RDHcxccj.mjs';
5
5
  export { Sql, isSql, joinSql, lit, raw, sql, toText } from '../packem_shared/Sql-DceGtcUd.mjs';
@@ -1,3 +1,3 @@
1
1
  export { createContextVectors, createVectorSyncHook } from '../packem_shared/createContextVectors-BSizpmu5.mjs';
2
- export { createVectorAdminIntrospector } from '../packem_shared/createVectorAdminIntrospector-BJUOM6VW.mjs';
3
- export { default as createVectors } from '../packem_shared/createVectors-LSpGoKCd.mjs';
2
+ export { createVectorAdminIntrospector } from '../packem_shared/createVectorAdminIntrospector-C-qTDWA1.mjs';
3
+ export { default as createVectors } from '../packem_shared/createVectors-DqxgEPQb.mjs';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lunora/bindings",
3
- "version": "1.0.0-alpha.2",
3
+ "version": "1.0.0-alpha.4",
4
4
  "description": "Lightweight Cloudflare binding helpers for Lunora — ctx.kv, ctx.images, ctx.analytics, ctx.pipelines, ctx.vectors, ctx.r2sql — one install, per-binding subpaths",
5
5
  "keywords": [
6
6
  "analytics",
@@ -63,6 +63,9 @@
63
63
  "publishConfig": {
64
64
  "access": "public"
65
65
  },
66
+ "dependencies": {
67
+ "@lunora/errors": "1.0.0-alpha.1"
68
+ },
66
69
  "engines": {
67
70
  "node": "^22.15.0 || >=24.11.0"
68
71
  }