@lunora/bindings 1.0.0-alpha.15 → 1.0.0-alpha.17
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/images/index.mjs +1 -1
- package/dist/packem_shared/buildSignedImageUrl-BzgCHIu6.mjs +4 -0
- package/dist/packem_shared/createContextVectors-D-ae1wWO.mjs +6 -0
- package/dist/vectors/index.d.mts +105 -4
- package/dist/vectors/index.d.ts +105 -4
- package/dist/vectors/index.mjs +1 -1
- package/package.json +3 -3
- package/dist/packem_shared/buildSignedImageUrl-Bpu3Hs42.mjs +0 -4
- package/dist/packem_shared/createContextVectors-HVMMJ-88.mjs +0 -6
package/dist/images/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createImages as m}from"../packem_shared/createImages-TW6ircG2.mjs";import{buildImageDeliveryUrl as i}from"../packem_shared/buildImageDeliveryUrl-eARoj3MG.mjs";import{buildSignedImageUrl as o,verifySignedImageUrl as a}from"../packem_shared/buildSignedImageUrl-
|
|
1
|
+
import{createImages as m}from"../packem_shared/createImages-TW6ircG2.mjs";import{buildImageDeliveryUrl as i}from"../packem_shared/buildImageDeliveryUrl-eARoj3MG.mjs";import{buildSignedImageUrl as o,verifySignedImageUrl as a}from"../packem_shared/buildSignedImageUrl-BzgCHIu6.mjs";export{i as buildImageDeliveryUrl,o as buildSignedImageUrl,m as createImages,a as verifySignedImageUrl};
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
const v=(e,r)=>{if(e.size<r)return;const t=e.keys().next().value;t!==void 0&&e.delete(t)},p=new TextEncoder,u=10080*60,U=/^[a-z][a-z0-9+\-.]*:\/\//i,$=Array.from({length:32},(e,r)=>r),S=new RegExp(`[${$.map(e=>String.fromCodePoint(e)).join("")}]`,"u"),x=e=>{const r=String.fromCodePoint(...e);return btoa(r).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")},A=e=>{const r=e.replaceAll("-","+").replaceAll("_","/")+"===".slice((e.length+3)%4),t=atob(r),n=new Uint8Array(t.length);for(let a=0;a<t.length;a+=1)n[a]=t.codePointAt(a)??0;return n},C=64,m=new Map,f=async e=>{const r=m.get(e);if(r)return r;v(m,C);const t=crypto.subtle.importKey("raw",p.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]);return m.set(e,t),t},g=e=>{try{return new URL(e).host}catch{return e.replace(U,"").split("/")[0]??""}},y=e=>{if(S.test(e))throw new TypeError("hmac-url: value must not contain control characters (including CR/LF)")},R=async(e,r)=>{const t=await f(e),n=await crypto.subtle.sign("HMAC",t,p.encode(r));return x(new Uint8Array(n))},I=async(e,r,t)=>{const n=await f(e);return crypto.subtle.verify("HMAC",n,t,p.encode(r))},h=/^\//,L=e=>e===void 0?"":Object.entries(e).filter(([,r])=>r!==void 0).toSorted(([r],[t])=>(r>t?1:0)-(r<t?1:0)).map(([r,t])=>`${r}=${typeof t=="object"?JSON.stringify(t):String(t)}`).join("&"),b=(e,r,t,n)=>`${e.toLowerCase()}
|
|
2
|
+
${r}
|
|
3
|
+
${String(t)}
|
|
4
|
+
${n}`,E=e=>e.split("/").map(r=>encodeURIComponent(r)).join("/"),N=async e=>{const r=e.expiresInSeconds??3600;if(!Number.isFinite(r)||r<=0)throw new TypeError("@lunora/bindings/images: expiresInSeconds must be a positive finite number");if(r>u)throw new TypeError(`@lunora/bindings/images: expiresInSeconds must not exceed ${String(u)} (7 days)`);let t="";try{t=new URL(e.baseUrl).pathname}catch{}if(t!==""&&t!=="/")throw new TypeError(`@lunora/bindings/images: baseUrl must not carry a path ("${t}") — the key is verified from the full URL pathname, so a subpath base would make every signed URL fail verification`);const n=Math.floor(Date.now()/1e3)+r,a=g(e.baseUrl),o=L(e.transform),s=e.key.replace(h,"");y(s);const l=await R(e.secret,b(a,s,n,o)),i=e.baseUrl.endsWith("/")?e.baseUrl.slice(0,-1):e.baseUrl,c=E(s),d=o===""?"":`&t=${encodeURIComponent(o)}`;return`${i}/${c}?exp=${String(n)}&sig=${l}${d}`},j=async(e,r,t)=>{let n;try{n=e instanceof URL?e:new URL(e)}catch{return{reason:"malformed",valid:!1}}const a=n.searchParams.get("exp"),o=a===null?Number.NaN:Number(a),s=n.searchParams.get("sig"),l=n.searchParams.get("t")??"";if(!s||!Number.isInteger(o))return{reason:"malformed",valid:!1};if(o<Math.floor(Date.now()/1e3))return{reason:"expired",valid:!1};let i,c;try{i=n.pathname.replace(h,"").split("/").map(w=>decodeURIComponent(w)).join("/"),y(i),c=A(s)}catch{return{reason:"malformed",valid:!1}}const d=t?.expectedHost===void 0?n.host:g(t.expectedHost);return await I(r,b(d,i,o,l),c)?{key:i,transform:l===""?void 0:l,valid:!0}:{reason:"bad_signature",valid:!1}};export{N as buildSignedImageUrl,j as verifySignedImageUrl};
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
import{c as w,U as b}from"./concurrent-DMFQCILU.mjs";const x=(n,c)=>{if(!c.includes("."))return n[c];let s=n;for(const l of c.split(".")){if(s===null||typeof s!="object"||Array.isArray(s))return;s=s[l]}return s},B=(n,c)=>{const s=c?.namespace,l=new Set(c?.shardedIndexNames),m=(t,a)=>{if(a!==void 0)return a;if(l.has(t)){if(s!==void 0)return s;throw new Error(`@lunora/bindings/vectors: index "${t}" belongs to a sharded table, but this DO instance has no shard key (it is the root/default DO) and no explicit namespace was given. A namespace-less operation here would reach every tenant's vectors — Vectorize indexes are account-global. Pass an explicit namespace, or issue this call from the sharded DO instance that owns the tenant.`)}},i=async(t,a)=>{await n.upsert(t,{embed:a.embed,id:a.id,input:a.input,metadata:a.metadata,namespace:m(t,a.namespace)})},u=async(t,a,r)=>{const o=await n.getByIds(t,a);return r===void 0?o:o.filter(d=>d.namespace===r)};return{deleteByIds:async(t,a,r)=>{const o=m(t,r);if(o===void 0){await n.deleteByIds(t,a);return}const d=await u(t,a,o);d.length!==0&&await n.deleteByIds(t,d.map(h=>h.id))},getByIds:async(t,a,r)=>{const o=m(t,r);return(await u(t,a,o)).map(d=>({id:d.id,metadata:d.metadata,namespace:d.namespace,values:d.values}))},query:async(t,a)=>{const r=await n.query(t,{embed:a.embed,filter:a.filter,input:a.input,namespace:m(t,a.namespace),returnMetadata:a.returnMetadata??"indexed",topK:a.topK,vector:a.vector});return{count:r.count,matches:r.matches.map(o=>({id:o.id,metadata:o.metadata,score:o.score}))}},upsert:i,upsertNow:i}},y=new Set,v=n=>{y.has(n)||(y.add(n),console.warn(`[@lunora/bindings/vectors] index "${n}" syncs vectors without a namespace — in a
|
|
2
|
+
multi-tenant/sharded app this exposes one tenant's vectors (and any captured
|
|
3
|
+
metadata) to every other tenant, since Vectorize indexes are account-global.
|
|
4
|
+
Pass \`namespace\` (the shard/tenant key) on both write and query — query-side
|
|
5
|
+
namespace filtering is mandatory for multi-tenant apps. Single-tenant apps that
|
|
6
|
+
legitimately have no tenant key suppress this via { allowSharedNamespace: true }.`))},g=(n,c)=>{const s={};for(const l of c)l in n&&(s[l]=n[l]);return s},S=n=>{const{allowSharedNamespace:c,namespace:s,schema:l,vectors:m}=n;return async i=>{const u=l.tables[i.table]?.vectorIndexes??[],t=Object.entries(l.vectorIndexes).filter(([,e])=>e.table===i.table);if(u.length===0&&t.length===0)return;const a=[...u.map(e=>e.name),...t.map(([e])=>e)];if(i.op==="delete"){await Promise.all(a.map(e=>m.deleteByIds(e,[i.id])));return}const r=i.doc;if(!r)return;const o=u.map(e=>({index:e,value:x(r,e.field)})),d=o.filter(e=>e.value!==void 0&&e.value!==null),h=o.filter(e=>e.value===void 0||e.value===null);for(const{index:e,value:p}of d)if(typeof p!="string")throw new TypeError(`@lunora/bindings/vectors: inline index "${e.name}" expects a string source at "${e.field}" on table "${i.table}" (got ${typeof p}); use a standalone defineVectorIndex with a select() to derive text from non-string columns`);const f=[...h.map(e=>async()=>{await m.deleteByIds(e.index.name,[i.id])}),...d.map(e=>async()=>{!c&&s===void 0&&v(e.index.name),await m.upsert(e.index.name,{embed:e.index.embed,id:i.id,input:e.value,metadata:e.index.metadata?g(r,e.index.metadata):void 0,namespace:s})}),...t.map(([e,p])=>async()=>{!c&&s===void 0&&v(e),await m.upsert(e,{embed:p.embed,id:i.id,input:p.select(r),metadata:p.metadata?.(r),namespace:s})})];try{await w(f,b,async e=>e())}catch(e){throw await Promise.allSettled(a.map(p=>m.deleteByIds(p,[i.id]))),e}}};export{B as createContextVectors,S as createVectorSyncHook};
|
package/dist/vectors/index.d.mts
CHANGED
|
@@ -57,6 +57,7 @@ interface VectorMatchesLike {
|
|
|
57
57
|
interface VectorRecordLike {
|
|
58
58
|
id: string;
|
|
59
59
|
metadata?: Record<string, unknown>;
|
|
60
|
+
namespace?: string;
|
|
60
61
|
values: ReadonlyArray<number>;
|
|
61
62
|
}
|
|
62
63
|
interface VectorQueryInputLike {
|
|
@@ -84,22 +85,105 @@ interface VectorUpsertInputLike {
|
|
|
84
85
|
/**
|
|
85
86
|
* Structural mirror of `@lunora/server`'s `VectorSearch`. Declared here so the
|
|
86
87
|
* adapter never imports `@lunora/server` (keeps the dependency edge one-way:
|
|
87
|
-
* the generated DO depends on both, neither depends on the other).
|
|
88
|
+
* the generated DO depends on both, neither depends on the other). `getByIds`/
|
|
89
|
+
* `deleteByIds` carry an optional trailing `namespace` — a pure addition (more
|
|
90
|
+
* general, not narrower) that stays assignable to `@lunora/server`'s
|
|
91
|
+
* `VectorSearchReader`/`VectorSearch`, whose own two-argument signatures are
|
|
92
|
+
* unchanged: a function accepting an extra OPTIONAL parameter is assignable
|
|
93
|
+
* wherever a function taking fewer parameters is expected.
|
|
88
94
|
*/
|
|
89
95
|
interface VectorSearchLike {
|
|
90
|
-
deleteByIds: (indexName: string, ids: ReadonlyArray<string
|
|
91
|
-
getByIds: (indexName: string, ids: ReadonlyArray<string
|
|
96
|
+
deleteByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<void>;
|
|
97
|
+
getByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<ReadonlyArray<VectorRecordLike>>;
|
|
92
98
|
query: (indexName: string, input: VectorQueryInputLike) => Promise<VectorMatchesLike>;
|
|
93
99
|
upsert: (indexName: string, input: VectorUpsertInputLike) => Promise<void>;
|
|
94
100
|
upsertNow: (indexName: string, input: VectorUpsertInputLike) => Promise<void>;
|
|
95
101
|
}
|
|
102
|
+
/** Options for {@link createContextVectors}. */
|
|
103
|
+
interface CreateContextVectorsOptions {
|
|
104
|
+
/**
|
|
105
|
+
* The DO's own shard/tenant key, applied as the default `namespace` for
|
|
106
|
+
* an operation against an index in `shardedIndexNames` that doesn't pass
|
|
107
|
+
* one explicitly. `undefined` means this instance HAS no shard key —
|
|
108
|
+
* always true for the root/default DO instance, since only a per-tenant
|
|
109
|
+
* instance owns one. See `shardedIndexNames` for what that implies per
|
|
110
|
+
* index, and {@link createContextVectors}'s docblock for the full
|
|
111
|
+
* root-instance rule.
|
|
112
|
+
*/
|
|
113
|
+
namespace?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Vector index names sourced from a `.shardBy()`'d table — the ones
|
|
116
|
+
* `namespace` is a meaningful tenant scope for. `ctx.vectors` is a single
|
|
117
|
+
* flat facade over EVERY declared index (root-scoped and sharded tables
|
|
118
|
+
* alike — Vectorize indexes are account-global and `config.vectors(env)`
|
|
119
|
+
* registers them all in one flat map), reachable from ANY DO instance —
|
|
120
|
+
* so `namespace` can only be a safe default for the indexes actually
|
|
121
|
+
* listed here.
|
|
122
|
+
*
|
|
123
|
+
* An index NOT in this set (sourced from a root-scoped table) always
|
|
124
|
+
* stays namespace-less, regardless of `namespace` or which DO instance
|
|
125
|
+
* calls it — it has no tenant identity to begin with, so scoping it would
|
|
126
|
+
* silently return nothing for legitimate, intentionally shared data (and,
|
|
127
|
+
* called from a per-tenant instance, would wrongly search under that
|
|
128
|
+
* tenant's namespace even though nothing was ever written there under
|
|
129
|
+
* it). An index IN this set, called from a per-tenant DO instance
|
|
130
|
+
* (`namespace` is set), defaults to `namespace`, scoping correctly. An
|
|
131
|
+
* index IN this set, called from the root/default DO instance
|
|
132
|
+
* (`namespace` is `undefined`) with no explicit override, is unsafe to
|
|
133
|
+
* default at all — see {@link createContextVectors}'s docblock.
|
|
134
|
+
*
|
|
135
|
+
* Omitted (or empty) → no index is ever treated as sharded, i.e.
|
|
136
|
+
* `namespace` never applies as a default on any call — the unsharded-app,
|
|
137
|
+
* byte-identical-to-today case.
|
|
138
|
+
*/
|
|
139
|
+
shardedIndexNames?: ReadonlyArray<string>;
|
|
140
|
+
}
|
|
96
141
|
/**
|
|
97
142
|
* Bridge `LunoraVectors` (returns Vectorize mutation receipts) to the server's
|
|
98
143
|
* `VectorSearch` contract (void mutations, server match/record shapes). Both
|
|
99
144
|
* `upsert` and `upsertNow` write inline — this design has no post-commit queue,
|
|
100
145
|
* so "now" and "deferred" collapse to the same synchronous call.
|
|
146
|
+
*
|
|
147
|
+
* Tenant isolation (read side) — IMPORTANT: an explicit `namespace` argument
|
|
148
|
+
* on any call (`input.namespace` for `query`/`upsert`/`upsertNow`, the
|
|
149
|
+
* trailing `namespace` parameter for `getByIds`/`deleteByIds`) ALWAYS wins —
|
|
150
|
+
* this is a deliberate soft default, not a hard boundary: `ctx.vectors` is
|
|
151
|
+
* trusted server-side app code (the same trust level that lets `ctx.db` read
|
|
152
|
+
* any table), so a caller that explicitly names a namespace is trusted to
|
|
153
|
+
* mean it, including a legitimate cross-tenant admin read/write. Absent an
|
|
154
|
+
* explicit namespace, `options.namespace` (this DO instance's own shard key)
|
|
155
|
+
* is the DEFAULT for any index in `options.shardedIndexNames` — see that
|
|
156
|
+
* option's docblock for why the default is index-scoped rather than global.
|
|
157
|
+
*
|
|
158
|
+
* Root-instance rule — IMPORTANT: when an operation targets a sharded index
|
|
159
|
+
* (one in `shardedIndexNames`) and BOTH the explicit argument and
|
|
160
|
+
* `options.namespace` are absent (this is the root/default DO instance, which
|
|
161
|
+
* owns no shard key), there is no safe default and no override — this THROWS
|
|
162
|
+
* rather than silently resolving to "no namespace". A namespace-less
|
|
163
|
+
* query/getByIds/deleteByIds/upsert against a sharded index would reach or
|
|
164
|
+
* mutate EVERY tenant's vectors (Vectorize indexes are account-global), which
|
|
165
|
+
* is the exact cross-tenant leak this file exists to close; returning an
|
|
166
|
+
* empty result set instead would masquerade that same configuration problem
|
|
167
|
+
* as "no data", which is worse — a caller debugging it sees nothing rather
|
|
168
|
+
* than a directed error. This case is reachable in a MIXED schema (some
|
|
169
|
+
* vectorized tables `.shardBy()`'d, others root-scoped) whenever application
|
|
170
|
+
* code queries a sharded index's name from the root DO instance without an
|
|
171
|
+
* explicit namespace; it is not reachable from `createVectorSyncHook`'s own
|
|
172
|
+
* internal calls, which only ever process a table this DO instance owns (so
|
|
173
|
+
* a sharded table's write never reaches a root instance in the first place).
|
|
174
|
+
*
|
|
175
|
+
* Id path, unrelated axis — IMPORTANT: independent of the override/root rules
|
|
176
|
+
* above, `getByIds`/`deleteByIds` can't ask Vectorize to filter by namespace
|
|
177
|
+
* remotely at all (its id-based operations take no `namespace` option), so
|
|
178
|
+
* once a namespace IS resolved (explicit or defaulted) for these two methods,
|
|
179
|
+
* isolation is enforced client-side: `getByIds` drops any returned record
|
|
180
|
+
* whose `namespace` doesn't match (fail closed: a record with no `namespace`
|
|
181
|
+
* field is treated as a mismatch, never as "belongs to everyone"), and
|
|
182
|
+
* `deleteByIds` resolves ids via `getByIds` first and only deletes the subset
|
|
183
|
+
* that belongs to the resolved namespace — silently, by design (see the
|
|
184
|
+
* `deleteByIds` implementation for the no-signal tradeoff this makes).
|
|
101
185
|
*/
|
|
102
|
-
declare const createContextVectors: (lunora: LunoraVectors) => VectorSearchLike;
|
|
186
|
+
declare const createContextVectors: (lunora: LunoraVectors, options?: CreateContextVectorsOptions) => VectorSearchLike;
|
|
103
187
|
/** A single row mutation observed by the ctx-db, fed to {@link createVectorSyncHook}. */
|
|
104
188
|
interface WriteEvent {
|
|
105
189
|
doc?: Record<string, unknown>;
|
|
@@ -152,6 +236,23 @@ interface SchemaLike {
|
|
|
152
236
|
* (regardless of whether metadata is present); a genuinely single-tenant app
|
|
153
237
|
* suppresses it with `allowSharedNamespace: true`.
|
|
154
238
|
*
|
|
239
|
+
* Since plan 255, codegen satisfies the query-side requirement automatically
|
|
240
|
+
* for a `.shardBy()`'d vectorized table: the `vectors` instance passed in
|
|
241
|
+
* `options` here is the SAME `createContextVectors(...)` instance exposed as
|
|
242
|
+
* `ctx.vectors`, constructed with the identical shard-key `namespace` default
|
|
243
|
+
* AND the identical `shardedIndexNames` — so `ctx.vectors.query`/`getByIds`/
|
|
244
|
+
* `deleteByIds` are scoped without any app code changes. One consequence of
|
|
245
|
+
* sharing that instance: this hook's own internal `deleteByIds` calls (on row
|
|
246
|
+
* delete, on a cleared inline field, and on compensation after a failed
|
|
247
|
+
* upsert) now also go through the namespace-verifying path described on
|
|
248
|
+
* {@link createContextVectors} — an extra `getByIds` subrequest per
|
|
249
|
+
* delete-shaped write, not a behavior change (the row being deleted was
|
|
250
|
+
* written under this same shard's namespace, so the verification passes).
|
|
251
|
+
* This never hits {@link createContextVectors}'s root-instance throw: a write
|
|
252
|
+
* event only ever fires for a table THIS DO instance owns, so if this hook
|
|
253
|
+
* processes a write for a sharded index, this instance IS a real per-tenant
|
|
254
|
+
* shard (not root) — `namespace` here is never `undefined` for that index.
|
|
255
|
+
*
|
|
155
256
|
* Consistency — IMPORTANT: this hook runs inline within the mutation but talks
|
|
156
257
|
* to Vectorize, which is external and non-transactional. The per-index calls
|
|
157
258
|
* fan out; if one fails after others have already applied, the SQLite write may
|
package/dist/vectors/index.d.ts
CHANGED
|
@@ -57,6 +57,7 @@ interface VectorMatchesLike {
|
|
|
57
57
|
interface VectorRecordLike {
|
|
58
58
|
id: string;
|
|
59
59
|
metadata?: Record<string, unknown>;
|
|
60
|
+
namespace?: string;
|
|
60
61
|
values: ReadonlyArray<number>;
|
|
61
62
|
}
|
|
62
63
|
interface VectorQueryInputLike {
|
|
@@ -84,22 +85,105 @@ interface VectorUpsertInputLike {
|
|
|
84
85
|
/**
|
|
85
86
|
* Structural mirror of `@lunora/server`'s `VectorSearch`. Declared here so the
|
|
86
87
|
* adapter never imports `@lunora/server` (keeps the dependency edge one-way:
|
|
87
|
-
* the generated DO depends on both, neither depends on the other).
|
|
88
|
+
* the generated DO depends on both, neither depends on the other). `getByIds`/
|
|
89
|
+
* `deleteByIds` carry an optional trailing `namespace` — a pure addition (more
|
|
90
|
+
* general, not narrower) that stays assignable to `@lunora/server`'s
|
|
91
|
+
* `VectorSearchReader`/`VectorSearch`, whose own two-argument signatures are
|
|
92
|
+
* unchanged: a function accepting an extra OPTIONAL parameter is assignable
|
|
93
|
+
* wherever a function taking fewer parameters is expected.
|
|
88
94
|
*/
|
|
89
95
|
interface VectorSearchLike {
|
|
90
|
-
deleteByIds: (indexName: string, ids: ReadonlyArray<string
|
|
91
|
-
getByIds: (indexName: string, ids: ReadonlyArray<string
|
|
96
|
+
deleteByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<void>;
|
|
97
|
+
getByIds: (indexName: string, ids: ReadonlyArray<string>, namespace?: string) => Promise<ReadonlyArray<VectorRecordLike>>;
|
|
92
98
|
query: (indexName: string, input: VectorQueryInputLike) => Promise<VectorMatchesLike>;
|
|
93
99
|
upsert: (indexName: string, input: VectorUpsertInputLike) => Promise<void>;
|
|
94
100
|
upsertNow: (indexName: string, input: VectorUpsertInputLike) => Promise<void>;
|
|
95
101
|
}
|
|
102
|
+
/** Options for {@link createContextVectors}. */
|
|
103
|
+
interface CreateContextVectorsOptions {
|
|
104
|
+
/**
|
|
105
|
+
* The DO's own shard/tenant key, applied as the default `namespace` for
|
|
106
|
+
* an operation against an index in `shardedIndexNames` that doesn't pass
|
|
107
|
+
* one explicitly. `undefined` means this instance HAS no shard key —
|
|
108
|
+
* always true for the root/default DO instance, since only a per-tenant
|
|
109
|
+
* instance owns one. See `shardedIndexNames` for what that implies per
|
|
110
|
+
* index, and {@link createContextVectors}'s docblock for the full
|
|
111
|
+
* root-instance rule.
|
|
112
|
+
*/
|
|
113
|
+
namespace?: string;
|
|
114
|
+
/**
|
|
115
|
+
* Vector index names sourced from a `.shardBy()`'d table — the ones
|
|
116
|
+
* `namespace` is a meaningful tenant scope for. `ctx.vectors` is a single
|
|
117
|
+
* flat facade over EVERY declared index (root-scoped and sharded tables
|
|
118
|
+
* alike — Vectorize indexes are account-global and `config.vectors(env)`
|
|
119
|
+
* registers them all in one flat map), reachable from ANY DO instance —
|
|
120
|
+
* so `namespace` can only be a safe default for the indexes actually
|
|
121
|
+
* listed here.
|
|
122
|
+
*
|
|
123
|
+
* An index NOT in this set (sourced from a root-scoped table) always
|
|
124
|
+
* stays namespace-less, regardless of `namespace` or which DO instance
|
|
125
|
+
* calls it — it has no tenant identity to begin with, so scoping it would
|
|
126
|
+
* silently return nothing for legitimate, intentionally shared data (and,
|
|
127
|
+
* called from a per-tenant instance, would wrongly search under that
|
|
128
|
+
* tenant's namespace even though nothing was ever written there under
|
|
129
|
+
* it). An index IN this set, called from a per-tenant DO instance
|
|
130
|
+
* (`namespace` is set), defaults to `namespace`, scoping correctly. An
|
|
131
|
+
* index IN this set, called from the root/default DO instance
|
|
132
|
+
* (`namespace` is `undefined`) with no explicit override, is unsafe to
|
|
133
|
+
* default at all — see {@link createContextVectors}'s docblock.
|
|
134
|
+
*
|
|
135
|
+
* Omitted (or empty) → no index is ever treated as sharded, i.e.
|
|
136
|
+
* `namespace` never applies as a default on any call — the unsharded-app,
|
|
137
|
+
* byte-identical-to-today case.
|
|
138
|
+
*/
|
|
139
|
+
shardedIndexNames?: ReadonlyArray<string>;
|
|
140
|
+
}
|
|
96
141
|
/**
|
|
97
142
|
* Bridge `LunoraVectors` (returns Vectorize mutation receipts) to the server's
|
|
98
143
|
* `VectorSearch` contract (void mutations, server match/record shapes). Both
|
|
99
144
|
* `upsert` and `upsertNow` write inline — this design has no post-commit queue,
|
|
100
145
|
* so "now" and "deferred" collapse to the same synchronous call.
|
|
146
|
+
*
|
|
147
|
+
* Tenant isolation (read side) — IMPORTANT: an explicit `namespace` argument
|
|
148
|
+
* on any call (`input.namespace` for `query`/`upsert`/`upsertNow`, the
|
|
149
|
+
* trailing `namespace` parameter for `getByIds`/`deleteByIds`) ALWAYS wins —
|
|
150
|
+
* this is a deliberate soft default, not a hard boundary: `ctx.vectors` is
|
|
151
|
+
* trusted server-side app code (the same trust level that lets `ctx.db` read
|
|
152
|
+
* any table), so a caller that explicitly names a namespace is trusted to
|
|
153
|
+
* mean it, including a legitimate cross-tenant admin read/write. Absent an
|
|
154
|
+
* explicit namespace, `options.namespace` (this DO instance's own shard key)
|
|
155
|
+
* is the DEFAULT for any index in `options.shardedIndexNames` — see that
|
|
156
|
+
* option's docblock for why the default is index-scoped rather than global.
|
|
157
|
+
*
|
|
158
|
+
* Root-instance rule — IMPORTANT: when an operation targets a sharded index
|
|
159
|
+
* (one in `shardedIndexNames`) and BOTH the explicit argument and
|
|
160
|
+
* `options.namespace` are absent (this is the root/default DO instance, which
|
|
161
|
+
* owns no shard key), there is no safe default and no override — this THROWS
|
|
162
|
+
* rather than silently resolving to "no namespace". A namespace-less
|
|
163
|
+
* query/getByIds/deleteByIds/upsert against a sharded index would reach or
|
|
164
|
+
* mutate EVERY tenant's vectors (Vectorize indexes are account-global), which
|
|
165
|
+
* is the exact cross-tenant leak this file exists to close; returning an
|
|
166
|
+
* empty result set instead would masquerade that same configuration problem
|
|
167
|
+
* as "no data", which is worse — a caller debugging it sees nothing rather
|
|
168
|
+
* than a directed error. This case is reachable in a MIXED schema (some
|
|
169
|
+
* vectorized tables `.shardBy()`'d, others root-scoped) whenever application
|
|
170
|
+
* code queries a sharded index's name from the root DO instance without an
|
|
171
|
+
* explicit namespace; it is not reachable from `createVectorSyncHook`'s own
|
|
172
|
+
* internal calls, which only ever process a table this DO instance owns (so
|
|
173
|
+
* a sharded table's write never reaches a root instance in the first place).
|
|
174
|
+
*
|
|
175
|
+
* Id path, unrelated axis — IMPORTANT: independent of the override/root rules
|
|
176
|
+
* above, `getByIds`/`deleteByIds` can't ask Vectorize to filter by namespace
|
|
177
|
+
* remotely at all (its id-based operations take no `namespace` option), so
|
|
178
|
+
* once a namespace IS resolved (explicit or defaulted) for these two methods,
|
|
179
|
+
* isolation is enforced client-side: `getByIds` drops any returned record
|
|
180
|
+
* whose `namespace` doesn't match (fail closed: a record with no `namespace`
|
|
181
|
+
* field is treated as a mismatch, never as "belongs to everyone"), and
|
|
182
|
+
* `deleteByIds` resolves ids via `getByIds` first and only deletes the subset
|
|
183
|
+
* that belongs to the resolved namespace — silently, by design (see the
|
|
184
|
+
* `deleteByIds` implementation for the no-signal tradeoff this makes).
|
|
101
185
|
*/
|
|
102
|
-
declare const createContextVectors: (lunora: LunoraVectors) => VectorSearchLike;
|
|
186
|
+
declare const createContextVectors: (lunora: LunoraVectors, options?: CreateContextVectorsOptions) => VectorSearchLike;
|
|
103
187
|
/** A single row mutation observed by the ctx-db, fed to {@link createVectorSyncHook}. */
|
|
104
188
|
interface WriteEvent {
|
|
105
189
|
doc?: Record<string, unknown>;
|
|
@@ -152,6 +236,23 @@ interface SchemaLike {
|
|
|
152
236
|
* (regardless of whether metadata is present); a genuinely single-tenant app
|
|
153
237
|
* suppresses it with `allowSharedNamespace: true`.
|
|
154
238
|
*
|
|
239
|
+
* Since plan 255, codegen satisfies the query-side requirement automatically
|
|
240
|
+
* for a `.shardBy()`'d vectorized table: the `vectors` instance passed in
|
|
241
|
+
* `options` here is the SAME `createContextVectors(...)` instance exposed as
|
|
242
|
+
* `ctx.vectors`, constructed with the identical shard-key `namespace` default
|
|
243
|
+
* AND the identical `shardedIndexNames` — so `ctx.vectors.query`/`getByIds`/
|
|
244
|
+
* `deleteByIds` are scoped without any app code changes. One consequence of
|
|
245
|
+
* sharing that instance: this hook's own internal `deleteByIds` calls (on row
|
|
246
|
+
* delete, on a cleared inline field, and on compensation after a failed
|
|
247
|
+
* upsert) now also go through the namespace-verifying path described on
|
|
248
|
+
* {@link createContextVectors} — an extra `getByIds` subrequest per
|
|
249
|
+
* delete-shaped write, not a behavior change (the row being deleted was
|
|
250
|
+
* written under this same shard's namespace, so the verification passes).
|
|
251
|
+
* This never hits {@link createContextVectors}'s root-instance throw: a write
|
|
252
|
+
* event only ever fires for a table THIS DO instance owns, so if this hook
|
|
253
|
+
* processes a write for a sharded index, this instance IS a real per-tenant
|
|
254
|
+
* shard (not root) — `namespace` here is never `undefined` for that index.
|
|
255
|
+
*
|
|
155
256
|
* Consistency — IMPORTANT: this hook runs inline within the mutation but talks
|
|
156
257
|
* to Vectorize, which is external and non-transactional. The per-index calls
|
|
157
258
|
* fan out; if one fails after others have already applied, the SQLite write may
|
package/dist/vectors/index.mjs
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createContextVectors as t,createVectorSyncHook as o}from"../packem_shared/createContextVectors-
|
|
1
|
+
import{createContextVectors as t,createVectorSyncHook as o}from"../packem_shared/createContextVectors-D-ae1wWO.mjs";import{createVectorAdminIntrospector as a}from"../packem_shared/createVectorAdminIntrospector-B9Yt09yj.mjs";import{default as m}from"../packem_shared/createVectors-jb8fAY6R.mjs";export{t as createContextVectors,a as createVectorAdminIntrospector,o as createVectorSyncHook,m as createVectors};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lunora/bindings",
|
|
3
|
-
"version": "1.0.0-alpha.
|
|
3
|
+
"version": "1.0.0-alpha.17",
|
|
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",
|
|
@@ -64,8 +64,8 @@
|
|
|
64
64
|
"access": "public"
|
|
65
65
|
},
|
|
66
66
|
"dependencies": {
|
|
67
|
-
"@lunora/errors": "1.0.0-alpha.
|
|
68
|
-
"@lunora/platform": "1.0.0-alpha.
|
|
67
|
+
"@lunora/errors": "1.0.0-alpha.12",
|
|
68
|
+
"@lunora/platform": "1.0.0-alpha.3"
|
|
69
69
|
},
|
|
70
70
|
"engines": {
|
|
71
71
|
"node": "^22.15.0 || >=24.11.0"
|
|
@@ -1,4 +0,0 @@
|
|
|
1
|
-
const w=(e,r)=>{if(e.size<r)return;const t=e.keys().next().value;t!==void 0&&e.delete(t)},m=new TextEncoder,u=10080*60,$=/^[a-z][a-z0-9+\-.]*:\/\//i,v=e=>{const r=String.fromCodePoint(...e);return btoa(r).replaceAll("+","-").replaceAll("/","_").replaceAll("=","")},U=e=>{const r=e.replaceAll("-","+").replaceAll("_","/")+"===".slice((e.length+3)%4),t=atob(r),n=new Uint8Array(t.length);for(let a=0;a<t.length;a+=1)n[a]=t.codePointAt(a)??0;return n},x=64,p=new Map,f=async e=>{const r=p.get(e);if(r)return r;w(p,x);const t=crypto.subtle.importKey("raw",m.encode(e),{hash:"SHA-256",name:"HMAC"},!1,["sign","verify"]);return p.set(e,t),t},g=e=>{try{return new URL(e).host}catch{return e.replace($,"").split("/")[0]??""}},A=async(e,r)=>{const t=await f(e),n=await crypto.subtle.sign("HMAC",t,m.encode(r));return v(new Uint8Array(n))},S=async(e,r,t)=>{const n=await f(e);return crypto.subtle.verify("HMAC",n,t,m.encode(r))},y=/^\//,I=e=>e===void 0?"":Object.entries(e).filter(([,r])=>r!==void 0).toSorted(([r],[t])=>(r>t?1:0)-(r<t?1:0)).map(([r,t])=>`${r}=${typeof t=="object"?JSON.stringify(t):String(t)}`).join("&"),h=(e,r,t,n)=>`${e.toLowerCase()}
|
|
2
|
-
${r}
|
|
3
|
-
${String(t)}
|
|
4
|
-
${n}`,R=e=>e.split("/").map(r=>encodeURIComponent(r)).join("/"),C=async e=>{const r=e.expiresInSeconds??3600;if(!Number.isFinite(r)||r<=0)throw new TypeError("@lunora/bindings/images: expiresInSeconds must be a positive finite number");if(r>u)throw new TypeError(`@lunora/bindings/images: expiresInSeconds must not exceed ${String(u)} (7 days)`);let t="";try{t=new URL(e.baseUrl).pathname}catch{}if(t!==""&&t!=="/")throw new TypeError(`@lunora/bindings/images: baseUrl must not carry a path ("${t}") — the key is verified from the full URL pathname, so a subpath base would make every signed URL fail verification`);const n=Math.floor(Date.now()/1e3)+r,a=g(e.baseUrl),s=I(e.transform),o=e.key.replace(y,""),i=await A(e.secret,h(a,o,n,s)),l=e.baseUrl.endsWith("/")?e.baseUrl.slice(0,-1):e.baseUrl,c=R(o),d=s===""?"":`&t=${encodeURIComponent(s)}`;return`${l}/${c}?exp=${String(n)}&sig=${i}${d}`},L=async(e,r,t)=>{let n;try{n=e instanceof URL?e:new URL(e)}catch{return{reason:"malformed",valid:!1}}const a=n.searchParams.get("exp"),s=a===null?Number.NaN:Number(a),o=n.searchParams.get("sig"),i=n.searchParams.get("t")??"";if(!o||!Number.isInteger(s))return{reason:"malformed",valid:!1};if(s<Math.floor(Date.now()/1e3))return{reason:"expired",valid:!1};let l,c;try{l=n.pathname.replace(y,"").split("/").map(b=>decodeURIComponent(b)).join("/"),c=U(o)}catch{return{reason:"malformed",valid:!1}}const d=t?.expectedHost===void 0?n.host:g(t.expectedHost);return await S(r,h(d,l,s,i),c)?{key:l,transform:i===""?void 0:i,valid:!0}:{reason:"bad_signature",valid:!1}};export{C as buildSignedImageUrl,L as verifySignedImageUrl};
|
|
@@ -1,6 +0,0 @@
|
|
|
1
|
-
import{c as w,U as b}from"./concurrent-DMFQCILU.mjs";const x=(n,d)=>{if(!d.includes("."))return n[d];let t=n;for(const a of d.split(".")){if(t===null||typeof t!="object"||Array.isArray(t))return;t=t[a]}return t},B=n=>{const d=async(t,a)=>{await n.upsert(t,{embed:a.embed,id:a.id,input:a.input,metadata:a.metadata,namespace:a.namespace})};return{deleteByIds:async(t,a)=>{await n.deleteByIds(t,a)},getByIds:async(t,a)=>(await n.getByIds(t,a)).map(i=>({id:i.id,metadata:i.metadata,values:i.values})),query:async(t,a)=>{const i=await n.query(t,{embed:a.embed,filter:a.filter,input:a.input,namespace:a.namespace,returnMetadata:a.returnMetadata??"indexed",topK:a.topK,vector:a.vector});return{count:i.count,matches:i.matches.map(s=>({id:s.id,metadata:s.metadata,score:s.score}))}},upsert:d,upsertNow:d}},y=new Set,v=n=>{y.has(n)||(y.add(n),console.warn(`[@lunora/bindings/vectors] index "${n}" syncs vectors without a namespace — in a
|
|
2
|
-
multi-tenant/sharded app this exposes one tenant's vectors (and any captured
|
|
3
|
-
metadata) to every other tenant, since Vectorize indexes are account-global.
|
|
4
|
-
Pass \`namespace\` (the shard/tenant key) on both write and query — query-side
|
|
5
|
-
namespace filtering is mandatory for multi-tenant apps. Single-tenant apps that
|
|
6
|
-
legitimately have no tenant key suppress this via { allowSharedNamespace: true }.`))},g=(n,d)=>{const t={};for(const a of d)a in n&&(t[a]=n[a]);return t},S=n=>{const{allowSharedNamespace:d,namespace:t,schema:a,vectors:i}=n;return async s=>{const c=a.tables[s.table]?.vectorIndexes??[],l=Object.entries(a.vectorIndexes).filter(([,e])=>e.table===s.table);if(c.length===0&&l.length===0)return;const m=[...c.map(e=>e.name),...l.map(([e])=>e)];if(s.op==="delete"){await Promise.all(m.map(e=>i.deleteByIds(e,[s.id])));return}const r=s.doc;if(!r)return;const u=c.map(e=>({index:e,value:x(r,e.field)})),p=u.filter(e=>e.value!==void 0&&e.value!==null),f=u.filter(e=>e.value===void 0||e.value===null);for(const{index:e,value:o}of p)if(typeof o!="string")throw new TypeError(`@lunora/bindings/vectors: inline index "${e.name}" expects a string source at "${e.field}" on table "${s.table}" (got ${typeof o}); use a standalone defineVectorIndex with a select() to derive text from non-string columns`);const h=[...f.map(e=>async()=>{await i.deleteByIds(e.index.name,[s.id])}),...p.map(e=>async()=>{!d&&t===void 0&&v(e.index.name),await i.upsert(e.index.name,{embed:e.index.embed,id:s.id,input:e.value,metadata:e.index.metadata?g(r,e.index.metadata):void 0,namespace:t})}),...l.map(([e,o])=>async()=>{!d&&t===void 0&&v(e),await i.upsert(e,{embed:o.embed,id:s.id,input:o.select(r),metadata:o.metadata?.(r),namespace:t})})];try{await w(h,b,async e=>e())}catch(e){throw await Promise.allSettled(m.map(o=>i.deleteByIds(o,[s.id]))),e}}};export{B as createContextVectors,S as createVectorSyncHook};
|