@lunora/server 1.0.0-alpha.31 → 1.0.0-alpha.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE.md +213 -0
- package/dist/data-model.mjs +0 -1
- package/dist/drizzle.mjs +1 -1
- package/dist/index.d.mts +106 -9
- package/dist/index.d.ts +106 -9
- package/dist/index.mjs +1 -29
- package/dist/otel.d.mts +543 -0
- package/dist/otel.d.ts +543 -0
- package/dist/otel.mjs +1 -0
- package/dist/packem_shared/LunoraEnvError-CgpI2Mm_.mjs +3 -0
- package/dist/packem_shared/LunoraError-LVhdU0Lo.mjs +1 -0
- package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-CF_9uiu1.mjs +1 -0
- package/dist/packem_shared/allowAll-BnyNbJZT.mjs +1 -0
- package/dist/packem_shared/asBucketStorage-1pFfH-Tn.mjs +1 -0
- package/dist/packem_shared/bindOrm-Bp9hsM2q.mjs +1 -0
- package/dist/packem_shared/buildRlsReadRegistry-WdiqSj87.mjs +1 -0
- package/dist/packem_shared/composePluginMiddleware-COr09CXA.mjs +1 -0
- package/dist/packem_shared/createPolicyDsl-sV1swpkD.mjs +1 -0
- package/dist/packem_shared/createSecrets-CgVPiW2C.mjs +1 -0
- package/dist/packem_shared/defineAggregateIndex-CYuH6IbJ.mjs +1 -0
- package/dist/packem_shared/defineIdentity-B7gfAgxx.mjs +1 -0
- package/dist/packem_shared/defineMigration-Bfpwxv2f.mjs +1 -0
- package/dist/packem_shared/defineMutator-8Kz0j1Kt.mjs +1 -0
- package/dist/packem_shared/defineShape-CyuRfwHz.mjs +1 -0
- package/dist/packem_shared/defineStorageRule-BDu01PUn.mjs +1 -0
- package/dist/packem_shared/functions-CDC08CWY.mjs +1 -0
- package/dist/packem_shared/httpAction-C14NuF3V.mjs +4 -0
- package/dist/packem_shared/initLunora-CHh1EmR4.mjs +1 -0
- package/dist/packem_shared/mask-C6Bi78qj.mjs +1 -0
- package/dist/packem_shared/onConnect-CEtRmUpJ.mjs +1 -0
- package/dist/packem_shared/policy-tag-Dprt9JWo.mjs +1 -0
- package/dist/packem_shared/protectPublic-BhKewPqm.mjs +1 -0
- package/dist/packem_shared/rls-_iVsPvhX.mjs +1 -0
- package/dist/packem_shared/run-middleware-BeEEqmdE.mjs +1 -0
- package/dist/packem_shared/storageRules-BptPZbi8.mjs +1 -0
- package/dist/rls/testing.mjs +1 -49
- package/dist/types.d.mts +211 -4
- package/dist/types.d.ts +211 -4
- package/dist/types.mjs +1 -31
- package/package.json +13 -1
- package/dist/packem_shared/LunoraEnvError-BGmd1Qs0.mjs +0 -187
- package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +0 -9
- package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-Cr0i4mTv.mjs +0 -114
- package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
- package/dist/packem_shared/bindOrm-CaY7Wq9Z.mjs +0 -147
- package/dist/packem_shared/buildRlsReadRegistry-2uk_GfiH.mjs +0 -107
- package/dist/packem_shared/composePluginMiddleware-z62dttBo.mjs +0 -103
- package/dist/packem_shared/createPolicyDsl-By3QB4he.mjs +0 -32
- package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +0 -58
- package/dist/packem_shared/defineAggregateIndex-cdo0g-un.mjs +0 -349
- package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +0 -35
- package/dist/packem_shared/defineMigration-Hx01yIht.mjs +0 -10
- package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +0 -11
- package/dist/packem_shared/defineShape-C5scNOrf.mjs +0 -18
- package/dist/packem_shared/defineStorageRule-B5nL4Z1P.mjs +0 -23
- package/dist/packem_shared/functions-Di9FUNkf.mjs +0 -5
- package/dist/packem_shared/httpAction-DCXoYPIk.mjs +0 -355
- package/dist/packem_shared/initLunora-D0Wuki7S.mjs +0 -115
- package/dist/packem_shared/mask-BepaW7YN.mjs +0 -334
- package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
- package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +0 -13
- package/dist/packem_shared/protectPublic-BlcGpiRc.mjs +0 -15
- package/dist/packem_shared/rls-BaDQf7MG.mjs +0 -595
- package/dist/packem_shared/run-middleware-I6EiQfxL.mjs +0 -20
- package/dist/packem_shared/storageRules-6QxzDOcx.mjs +0 -88
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import{toErrorBody as E}from"@lunora/errors";import{parseValidatorMap as R,ValidationError as v}from"@lunora/values";import{Hono as P}from"hono";import{LunoraError as p}from"./LunoraError-LVhdU0Lo.mjs";const H=e=>async t=>e(t.get("lunora"),t.req.raw),z=()=>{const e=new P;return e.use("*",async(t,a)=>{const r=t.env.__lunoraCtx;if(!r)throw new p("INTERNAL_SERVER_ERROR","HttpActionCtx was not injected — mount httpRouter() on createWorker(), which supplies it per request.");t.set("lunora",r),await a()}),e},k=e=>e.kind==="optional"?e._meta?.inner??e:e,y=(e,t)=>{switch(e){case"bigint":try{return BigInt(t)}catch{return t}case"boolean":return t==="true"||t==="1"?!0:t==="false"||t==="0"?!1:t;case"number":return t===""?Number.NaN:Number(t);default:return t}},_=(e,t,a)=>{const r=k(e);if(r.kind==="array"){const o=t.req.queries(a);if(o===void 0)return;const n=r._meta?.inner;return o.map(c=>y(n?.kind??"string",c))}const s=t.req.query(a);return s===void 0?void 0:y(r.kind,s)},S=(e,t)=>{const a={};for(const r of Object.keys(e)){const s=e[r];s&&(a[r]=_(s,t,r))}return R(e,a,"searchParams")},N=(e,t)=>{const a=t.req.param(),r={};for(const s of Object.keys(e)){const o=e[s];if(!o)continue;const n=a[s];r[s]=n===void 0?void 0:y(k(o).kind,n)}return R(e,r,"params")},x=async(e,t)=>{let a;try{a=await t.req.json()}catch{throw new p("BAD_REQUEST","Invalid JSON body")}if(typeof a!="object"||a===null||Array.isArray(a))throw new p("BAD_REQUEST","Expected a JSON object body");return R(e,a,"body")},A=(e,t)=>{try{return e.parse(t)}catch(a){throw a instanceof v?new p("INTERNAL_SERVER_ERROR",`Response did not match the declared output schema: ${a.message}`):a}},O=e=>{if(e instanceof v)return Response.json({code:"BAD_REQUEST",error:e.message},{status:400});if(e instanceof p){const{body:t,redacted:a,status:r}=E(e,{fallbackCode:"INTERNAL_SERVER_ERROR",redactedMessage:"Internal error"});return a&&console.error("[lunora] http action error (redacted on the wire):",e),Response.json({code:t.code,error:t.message},{status:r})}throw e},q=(e,t)=>async a=>{try{const r=a.get("lunora"),s=Object.keys(e.searchParams).length>0?S(e.searchParams,a):{},o=Object.keys(e.params).length>0?N(e.params,a):{},n=Object.keys(e.body).length>0?await x(e.body,a):{},c=await t({body:n,ctx:r,params:o,searchParams:s}),i=e.output?A(e.output,c):c,u={};e.cacheControl&&(u["cache-control"]=e.cacheControl),e.cacheTag&&(u["cache-tag"]=e.cacheTag),e.vary&&(u.vary=e.vary);const m=Object.keys(u).length>0;return i===void 0?new Response(null,{headers:m?u:void 0,status:204}):Response.json(i,{headers:m?u:void 0})}catch(r){return O(r)}},g=(e,t)=>{const a=JSON.stringify(e);return`${t?`event: ${t}
|
|
2
|
+
`:""}data: ${a}
|
|
3
|
+
|
|
4
|
+
`},C=(e,t)=>async a=>{let r,s;try{r=Object.keys(e.searchParams).length>0?S(e.searchParams,a):{},s=Object.keys(e.params).length>0?N(e.params,a):{}}catch(d){return O(d)}const o=a.get("lunora"),n=a.req.raw,c=new TextEncoder,i=new AbortController;if(n.signal.aborted)return i.abort(),new Response(new ReadableStream({start:d=>{d.close()}}),{headers:{"cache-control":"no-cache, no-transform","content-type":"text/event-stream; charset=utf-8","x-accel-buffering":"no"}});const u=()=>{i.abort()};n.signal.addEventListener("abort",u,{once:!0});const m=new ReadableStream({cancel(){n.signal.removeEventListener("abort",u),i.abort()},async start(d){try{const f=t({ctx:o,params:s,request:n,searchParams:r,signal:i.signal});for await(const b of f){if(i.signal.aborted)break;d.enqueue(c.encode(g(b)))}d.enqueue(c.encode(g({},"complete")))}catch(f){const{body:b,redacted:j}=E(f,{fallbackCode:"INTERNAL_SERVER_ERROR",redactedMessage:"Internal error"});j&&console.error("[lunora] unhandled stream handler error:",f),d.enqueue(c.encode(g({code:b.code,message:b.message},"error")))}finally{n.signal.removeEventListener("abort",u),d.close()}}}),T={"cache-control":"no-cache, no-transform","content-type":"text/event-stream; charset=utf-8","x-accel-buffering":"no"};return new Response(m,{headers:T})},l=e=>({body:t=>l({...e,body:{...e.body,...t}}),cacheControl:t=>l({...e,cacheControl:t}),cacheTag:t=>l({...e,cacheTag:t}),handler:t=>q(e,t),output:t=>l({...e,output:t}),params:t=>l({...e,params:{...e.params,...t}}),searchParams:t=>l({...e,searchParams:{...e.searchParams,...t}}),stream:t=>C(e,t),vary:t=>l({...e,vary:t})}),h=e=>t=>l({body:{},method:e,params:{},path:t,searchParams:{}}),U={delete:h("DELETE"),get:h("GET"),head:h("HEAD"),options:h("OPTIONS"),patch:h("PATCH"),post:h("POST"),put:h("PUT")},$=/^bytes=(\d*)-(\d*)$/,w=e=>e.startsWith('"')||e.startsWith('W/"')?e:`"${e}"`,I=e=>{for(let t=0;t<e.length;t+=1){const a=e.codePointAt(t);if(a===13||a===10||a===0)return!1}return!0},L=(e,t)=>{if(e===null)return{kind:"full"};const a=$.exec(e.trim());if(!a)return{kind:"full"};const r=a[1]??"",s=a[2]??"";if(r===""&&s==="")return{kind:"full"};let o,n;if(r===""){const c=Number(s);if(c===0)return{kind:"unsatisfiable"};o=Math.max(0,t-c),n=t-1}else o=Number(r),n=s===""?t-1:Math.min(Number(s),t-1);return o>n||o>=t?{kind:"unsatisfiable"}:{end:n,kind:"partial",start:o}},W=async(e,t,a)=>{const r=await e.storage.download(t);if(!r)return new Response("Not Found",{status:404});const s=r.httpMetadata?.contentType,o={"accept-ranges":"bytes","content-type":s!==void 0&&I(s)?s:"application/octet-stream",etag:w(r.etag)};r.sha256Base64!==void 0&&(o["repr-digest"]=`sha-256=:${r.sha256Base64}:`);const n=L(a.headers.get("range"),r.size);if(n.kind==="unsatisfiable")return r.body?.cancel().catch(()=>{}),new Response("Range Not Satisfiable",{headers:{"accept-ranges":"bytes","content-range":`bytes */${String(r.size)}`,"content-type":"text/plain; charset=utf-8",etag:w(r.etag)},status:416});if(n.kind==="full")return new Response(r.body,{headers:{...o,"content-length":String(r.size)},status:200});r.body?.cancel().catch(()=>{});const c=n.end-n.start+1,i=await e.storage.download(t,{range:{length:c,offset:n.start}});return i?new Response(i.body,{headers:{...o,"content-length":String(c),"content-range":`bytes ${String(n.start)}-${String(n.end)}/${String(r.size)}`},status:206}):new Response("Not Found",{status:404})};export{H as httpAction,U as httpRoute,z as httpRouter,I as isSafeHeaderValue,W as serveStorageObject};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{s as c}from"./functions-CDC08CWY.mjs";import{l as g}from"./policy-tag-Dprt9JWo.mjs";import{l as p}from"./run-middleware-BeEEqmdE.mjs";const x=(r,e)=>p(r,e,a=>a),w=(r,e,a,t)=>async(i,o)=>{const d=c(r,o),n=await x(e,i),l=await a({args:d,ctx:n});return t?t.parse(l):l},y=(r,e,a)=>(t,i,o)=>{const d=c(r,i);return(async function*(){const n=await x(e,t),l=a({args:d,ctx:n,signal:o})[Symbol.asyncIterator]();try{for(;;){if(o.aborted)return;const u=await l.next();if(u.done||o.aborted)return;yield u.value}}finally{await l.return?.()}})()},m=r=>{const e=[];for(const a of r){const t=g(a);t&&e.push(t)}return e.length>0?{tags:e}:void 0},s=(r,e,a)=>({__lunoraProcedure:r,...a?{__lunoraVisibility:a}:{},input:t=>s(r,{...e,args:{...e.args,...t}},a),[r]:t=>{const i=m(e.middlewares);return{args:e.args,...e.expose?{expose:e.expose}:{},handler:w(e.args,e.middlewares,t,e.output),kind:r,...i?{rls:i}:{},...a?{visibility:a}:{},...e.x402?{x402:e.x402}:{}}},output:t=>s(r,{...e,output:t},a),...r==="query"?{stream:t=>{const i=m(e.middlewares);return{args:e.args,...e.expose?{expose:e.expose}:{},handler:y(e.args,e.middlewares,t),kind:"stream",...i?{rls:i}:{},...a?{visibility:a}:{},...e.x402?{x402:e.x402}:{}}}}:{},use:t=>s(r,{...e,middlewares:[...e.middlewares,t]},a),...a?{}:{expose:t=>s(r,{...e,expose:t},a)},...a?{}:{x402:t=>s(r,{...e,x402:t},a)}}),q={dataModel:()=>({create:r=>({action:s("action",{args:{},middlewares:[]}),internalAction:s("action",{args:{},middlewares:[]},"internal"),internalMutation:s("mutation",{args:{},middlewares:[]},"internal"),internalQuery:s("query",{args:{},middlewares:[]},"internal"),mutation:s("mutation",{args:{},middlewares:[]}),query:s("query",{args:{},middlewares:[]})})})};export{q as initLunora};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as g}from"./LunoraError-LVhdU0Lo.mjs";import{bindOrm as N,bindTableFacade as x}from"./bindOrm-Bp9hsM2q.mjs";const S=a=>typeof a=="string"?a:a.name,I=a=>{const i=new Map;for(const s of a??[])i.set(s.name,new Set((s.permissions??[]).map(c=>S(c))));return i},M=a=>{let i=2166136261;for(let s=0;s<a.length;s+=1)i^=a.codePointAt(s)??0,i=Math.imul(i,16777619);return(i>>>0).toString(16).padStart(8,"0")},O=(a,i,s)=>{try{return a==="redact"?null:a==="hash"?i==null?i:M(typeof i=="string"?i:JSON.stringify(i)):a(i,s)}catch{return null}},h=(a,i,s)=>{const c={...a};for(const[f,w]of Object.entries(i))f in c&&(c[f]=O(w,a[f],{...s,column:f,row:a}));return c},m=(a,i,s)=>({...a,page:a.page.map(c=>h(c,i,s))}),A=(a,i,s,c)=>{if(typeof a!="function")return;const f=new Set,w=()=>new Proxy({},{get:()=>y=>(typeof y=="string"&&f.add(y),w())});a(w());for(const y of f)if(y in i)throw new g("MASK_UNSUPPORTED",`${c}() filtering "${s}" by masked column "${y}" is not supported`)},k=a=>{if(typeof a!="object"||a===null||Array.isArray(a))return!1;const i=a;return typeof i.findMany=="function"&&typeof i.withSearchIndex=="function"},v=(a,i,s)=>{const c=(t,r,n)=>({collect:async()=>(await t.collect()).map(e=>h(e,r,s)),filter:e=>c(t.filter(o=>e(h(o,r,s))),r,n),first:async()=>{const e=await t.first();return e?h(e,r,s):null},order:e=>c(t.order(e),r,n),paginate:async e=>m(await t.paginate(e),r,s),take:async e=>(await t.take(e)).map(o=>h(o,r,s)),unique:async()=>{const e=await t.unique();return e?h(e,r,s):null},withIndex:(e,o)=>(A(o,r,n,"withIndex"),c(t.withIndex(e,o),r,n)),withSearchIndex:(e,o)=>(A(o,r,n,"withSearchIndex"),c(t.withSearchIndex(e,o),r,n)),withGeoIndex:(e,o)=>c(t.withGeoIndex(e,o),r,n)}),f=async(t,r)=>{if(a.lookupById){const d=await a.lookupById(t,r);return d?{row:d.row,tableName:i.has(d.tableName)?d.tableName:void 0}:{row:null,tableName:void 0}}const n=await a.get(t,r);if(!n)return{row:null,tableName:void 0};let e;r===void 0?e=[...i.keys()]:i.has(r)?e=[r]:e=[];const o=await Promise.all(e.map(async d=>(await a.findFirst(d,{limit:1,where:{_id:t}}))?._id===t?d:void 0));return{row:n,tableName:o.find(d=>d!==void 0)}},w=(t,r,n)=>{const e=i.get(t);if(!e)return;const o=r.find(d=>typeof d=="string"&&d in e);if(o!==void 0)throw new g("MASK_UNSUPPORTED",`${n}() over masked column "${o}" on "${t}" is not supported`)},y=(t,r)=>{if(!(!t||typeof t!="object"||Array.isArray(t)))for(const[n,e]of Object.entries(t))if(n==="AND"||n==="OR"){if(Array.isArray(e))for(const o of e)y(o,r)}else n==="NOT"?y(e,r):n.startsWith("__")||r.add(n)},u=(t,r,n)=>{const e=i.get(t);if(!e||r===void 0)return;const o=new Set;y(r,o);for(const d of o)if(d in e)throw new g("MASK_UNSUPPORTED",`${n}() filtering "${t}" by masked column "${d}" is not supported`)},l=(t,r,n)=>{const e=i.get(t);if(!(!e||!Array.isArray(r))){for(const o of r)if(o&&typeof o=="object"&&!Array.isArray(o)){for(const d of Object.keys(o))if(d in e)throw new g("MASK_UNSUPPORTED",`${n}() ordering "${t}" by masked column "${d}" is not supported`)}}},p={...a,async deleteWhere(t,r,n){if(u(t,r,"deleteMany({ where })"),a.deleteWhere===void 0)throw new g("INTERNAL",`ctx.db.${t}.deleteMany({ where }) is unavailable: this writer has no where-based delete`);return a.deleteWhere(t,r,n)},async patchWhere(t,r,n){if(u(t,r.where,"patchMany({ where })"),a.patchWhere===void 0)throw new g("INTERNAL",`ctx.db.${t}.patchMany({ where }) is unavailable: this writer has no where-based patch`);return a.patchWhere(t,r,n)},aggregate(t,r){return w(t,[r.field],"aggregate"),u(t,r.where,"aggregate"),a.aggregate(t,r)},count(t,r){const n=r&&typeof r=="object"&&!Array.isArray(r)?r:void 0,e=n&&("where"in n||"baseWhere"in n||"restrictsCounts"in n)?n.where:r;return u(t,e,"count"),n&&u(t,n.baseWhere,"count"),a.count(t,r)},async findFirst(t,r){u(t,r?.where,"findFirst"),u(t,r?.baseWhere,"findFirst"),l(t,r?.orderBy,"findFirst");const n=await a.findFirst(t,r),e=i.get(t);return n&&e?h(n,e,s):n},async findFirstOrThrow(t,r){u(t,r?.where,"findFirstOrThrow"),u(t,r?.baseWhere,"findFirstOrThrow"),l(t,r?.orderBy,"findFirstOrThrow");const n=await a.findFirstOrThrow(t,r),e=i.get(t);return e?h(n,e,s):n},async findMany(t,r){u(t,r?.where,"findMany"),u(t,r?.baseWhere,"findMany"),l(t,r?.orderBy,"findMany");const n=await a.findMany(t,r),e=i.get(t);return e?m(n,e,s):n},async get(t,r){const{row:n,tableName:e}=await f(t,r),o=e===void 0?void 0:i.get(e);return!n||!o?n:h(n,o,s)},groupBy(t,r){return w(t,[...r.by,r.agg?.field],"groupBy"),u(t,r.where,"groupBy"),a.groupBy(t,r)},query(t){const r=a.query(t),n=i.get(t);return n?c(r,n,t):r},async rankPage(t,r,n){const e=await a.rankPage(t,r,n),o=i.get(t);return o?m(e,o,s):e}},b=p;for(const t of i.keys())k(a[t])&&(b[t]=x(p,t));return p},F=(a,i={})=>{const s=new Map(Object.entries(a)),c=I(i.roles);return async({ctx:f,next:w})=>{const y=f.auth??{},u=await y.getIdentity?.()??null,l=y.roles??[],p=new Set;for(const e of l)for(const o of c.get(e)??[])p.add(o);const b={auth:{can:e=>p.has(S(e)),identity:u,roles:l,userId:y.userId??null},ctx:f};if(i.bypass?.(b))return w();const t=v(f.db,s,b),r={db:t},{orm:n}=f;return n!==null&&typeof n=="object"&&(r.orm=N(t)),w({ctx:r})}};export{F as mask};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const i=(n,c)=>({args:{},handler:(t,e)=>c(t,e),kind:"mutation",lifecycle:n,visibility:"internal"}),o=n=>i("connect",n),s=n=>i("disconnect",n);export{o as onConnect,s as onDisconnect};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
const o=Symbol.for("lunora.rls.middleware-policies"),l=(e,r)=>(Object.defineProperty(e,o,{configurable:!0,enumerable:!1,value:r}),e),t=e=>{if(!(e===null||typeof e!="function"&&typeof e!="object"))return e[o]};export{t as l,l as r};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{l as n}from"./run-middleware-BeEEqmdE.mjs";const e=t=>{const r=[t.rateLimit,t.captcha,...t.use??[]].filter(c=>c!==void 0);return async({ctx:c,next:o})=>n(r,c,i=>o({ctx:i}))};export{e as protectPublic};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as p}from"./LunoraError-LVhdU0Lo.mjs";import{bindOrm as x,bindTableFacade as P}from"./bindOrm-Bp9hsM2q.mjs";import{r as U}from"./policy-tag-Dprt9JWo.mjs";import{deny as j}from"./allowAll-BnyNbJZT.mjs";const S=500,N=(t,r,s)=>{const i=r??S;if(t>i)throw new p("BAD_REQUEST",`${s}: batch of ${String(t)} exceeds the limit of ${String(i)} (raise options.limit or chunk the call)`)},F=j(),C=Symbol.for("lunora.ctxdb.rls-unwrap"),$=t=>{const r=new Map;for(const s of t){const i=r.get(s.table)??[];i.push(s),r.set(s.table,i)}return r},q=(t,r)=>{const s=[];let i=!1;for(const u of t){if(u.on!=="read")continue;const c=u.when(r);if(c!==void 0){if(c===!0){i=!0;break}c!==!1&&s.push(c)}}if(!i)return s.length===0?F:s.length===1?s[0]:{OR:s}},D=t=>typeof t=="object"&&t!==null&&!Array.isArray(t),L=["contains","eq","gt","gte","in","isNull","lt","lte","ne","notIn"],m=t=>{const r=typeof t;return r==="number"||r==="string"||r==="bigint"},z=(t,r)=>!("lt"in r&&(!m(t)||!m(r.lt)||t>=r.lt)||"lte"in r&&(!m(t)||!m(r.lte)||t>r.lte)||"gt"in r&&(!m(t)||!m(r.gt)||t<=r.gt)||"gte"in r&&(!m(t)||!m(r.gte)||t<r.gte)),Q=(t,r)=>{if("in"in r){const s=r.in;if(!Array.isArray(s)||!s.includes(t))return!1}if("notIn"in r){const s=r.notIn;if(Array.isArray(s)&&s.includes(t))return!1}if("contains"in r){const s=r.contains;if(typeof t!="string"||typeof s!="string"||!t.includes(s))return!1}return!("isNull"in r&&r.isNull===!0!=(t==null))},G=(t,r)=>"eq"in r&&t!==r.eq||"ne"in r&&t===r.ne?!1:Q(t,r)&&z(t,r),H=(t,r,s,i)=>r==="AND"?Array.isArray(s)&&s.every(u=>i(t,u)):r==="OR"?Array.isArray(s)&&s.some(u=>i(t,u)):!i(t,s??{}),I=t=>t==="AND"||t==="OR"||t==="NOT",J=t=>D(t)&&Object.keys(t).every(r=>L.includes(r)),K=["every","is","isNot","none","some"],V=t=>D(t)&&Object.keys(t).length>0&&Object.keys(t).every(r=>K.includes(r)),R=t=>Object.keys(t).some(r=>{const s=t[r];return I(r)?r==="NOT"?R(s??{}):Array.isArray(s)&&s.some(i=>R(i??{})):V(s)}),k=(t,r)=>{for(const s of Object.keys(r)){const i=r[s];if(I(s)){if(!H(t,s,i,k))return!1;continue}const u=t[s];if(J(i)){if(!G(u,i))return!1;continue}if(u!==i)return!1}return!0},X=(t,r,s)=>{const i=t.when({...r,row:s});if(i===void 0||i===!0)return!0;if(i===!1)return!1;if(R(i))throw new p("RELATION_PREDICATE_UNSUPPORTED","relation predicates are not supported in write policies; use a read policy or a flat column check");return k(s,i)},Y=(t,r,s)=>{const i=t.when(r);if(i===void 0||i===!0)return s===void 0||X(t,r,s);if(i===!1)return!1;if(R(i))throw new p("RELATION_PREDICATE_UNSUPPORTED","relation predicates are not supported in write policies; use a read policy or a flat column check");return!r.row||!k(r.row,i)?!1:s===void 0||k(s,i)},M=(t,r,s,i)=>{let u=!1;for(const c of t)if(c.on===r&&(u=!0,!Y(c,s,i)))return!1;return u},E=t=>t??{},Z=new Set(["baseWhere","relationBaseWhere","restrictsCounts","where"]),ee=(t,r)=>r===void 0?!0:t==="restrictsCounts"?typeof r=="boolean":t==="relationBaseWhere"?typeof r=="function":D(r),te=t=>{const r=Object.keys(t);return r.length===0?!1:r.every(s=>Z.has(s)&&ee(s,t[s]))},re=t=>t===void 0?{}:D(t)&&te(t)?t:{where:t},b=(t,r)=>!r||Object.keys(r).length===0?t:!t||Object.keys(t).length===0?r:{AND:[r,t]},ne=t=>{if(typeof t!="object"||t===null||Array.isArray(t))return!1;const r=t;return typeof r.findMany=="function"&&typeof r.withSearchIndex=="function"},ae=(t,r,s,i)=>{const u=new Map,c=e=>{const n=u.get(e);if(n)return n;const a=s.get(e);if(!a||a.length===0||!a.some(l=>l.on==="read")){const l={baseWhere:void 0,restricts:!1};return u.set(e,l),l}const o={baseWhere:q(a,i),restricts:!0};return u.set(e,o),o},f=e=>c(e).restricts?r:t,y=e=>c(e).baseWhere,v=async(e,n)=>{if(r.lookupById){const h=await r.lookupById(e,n);return h?{row:h.row,tableName:s.has(h.tableName)?h.tableName:void 0}:{row:null,tableName:void 0}}const a=await r.get(e,n);if(!a)return{row:null,tableName:void 0};const o=n!==void 0&&s.has(n)?[n]:[],l=n===void 0?[...s.keys()]:o,d=await Promise.all(l.map(async h=>(await r.findFirst(h,{limit:1,where:{_id:e}}))?._id===e?h:void 0));return{row:a,tableName:d.find(h=>h!==void 0)}},B=async(e,n)=>{const a=await v(e,n);return a.row&&a.tableName!==void 0?{row:a.row,tableName:a.tableName}:void 0},w=async(e,n,a,o,l)=>{const d=await B(e,l);if(!d)return a(t);const h=s.get(d.tableName);if(h){const _=o?o(d.row):void 0;if(!M(h,n,{...i,row:d.row},_))throw new p("FORBIDDEN",`${n} denied by policy`)}return a(r)},W=(e,n)=>{const{baseWhere:a,restricts:o}=c(e);if(o)throw new p("COUNT_RLS_UNSUPPORTED",`${n}() is not supported on "${e}" inside an RLS-restricted context`);return a},O=t.rankBefore,g={...t,async count(e,n){const{baseWhere:a,restricts:o}=c(e),l=re(n);return f(e).count(e,{...l,baseWhere:b(l.baseWhere,a),relationBaseWhere:y,restrictsCounts:(l.restrictsCounts??!1)||o})},delete:(e,n,a)=>w(e,"delete",o=>o.delete(e,n,a),void 0,n),async deleteAll(e,n){const a=Math.max(1,n?.chunkSize??S),{baseWhere:o}=c(e);let l=0;for(;;){const d=(await f(e).findMany(e,{baseWhere:o,limit:a,relationBaseWhere:y})).page.map(h=>String(h._id));if(d.length===0)break;for(const h of d)await w(h,"delete",_=>_.delete(h,void 0,n?.hard===void 0?void 0:{hard:n.hard})),l+=1;if(d.length<a)break}return{deleted:l}},async deleteMany(e,n,a){N(e.length,n?.limit,"deleteMany");for(const o of e)await w(o,"delete",l=>l.delete(o,a),void 0,a);return{deleted:e.length}},async deleteWhere(e,n,a){const{baseWhere:o}=c(e),l=(await f(e).findMany(e,{baseWhere:b(n,o),relationBaseWhere:y})).page.map(d=>String(d._id));return N(l.length,a?.limit,"deleteWhere"),g.deleteMany(l,a)},async findFirst(e,n){const{baseWhere:a}=c(e);return f(e).findFirst(e,{...E(n),baseWhere:b(n?.baseWhere,a),relationBaseWhere:y})},async findFirstOrThrow(e,n){const{baseWhere:a}=c(e);return f(e).findFirstOrThrow(e,{...E(n),baseWhere:b(n?.baseWhere,a),relationBaseWhere:y})},async findMany(e,n){const{baseWhere:a}=c(e);return f(e).findMany(e,{...E(n),baseWhere:b(n?.baseWhere,a),relationBaseWhere:y})},async get(e,n){const a=await v(e,n);if(!a.row)return null;if(a.tableName===void 0)return t.get(e,n);const{baseWhere:o,restricts:l}=c(a.tableName);if(!l)return t.get(e,n);if(!o)return a.row;const d=await r.findFirst(a.tableName,{baseWhere:o,limit:1,where:{_id:e}});return d?._id===e?d:null},async insert(e,n){const a=s.get(e);if(a){if(!M(a,"insert",{...i,row:n}))throw new p("FORBIDDEN",`insert on "${e}" denied by policy`);return r.insert(e,n)}return t.insert(e,n)},async insertMany(e,n,a){N(n.length,a?.limit,"insertMany");const o=s.get(e);if(o){for(const l of n)if(!M(o,"insert",{...i,row:l}))throw new p("FORBIDDEN",`insert on "${e}" denied by policy`);return r.insertMany(e,n,a)}return t.insertMany(e,n,a)},async insertManyUnsafe(e,n,a){N(n.length,a?.limit,"insertManyUnsafe");const o=s.get(e);if(o){for(const l of n)if(!M(o,"insert",{...i,row:l}))throw new p("FORBIDDEN",`insert on "${e}" denied by policy`);return r.insertManyUnsafe(e,n,a)}return t.insertManyUnsafe(e,n,a)},patch:(e,n,a)=>w(e,"update",o=>o.patch(e,n,a),o=>({...o,...n}),a),async patchMany(e,n,a){N(e.length,n?.limit,"patchMany");for(const o of e)await w(o.id,"update",l=>l.patch(o.id,o.patch,a),l=>({...l,...o.patch}),a);return{patched:e.length}},async patchWhere(e,n,a){const{baseWhere:o}=c(e),l=(await f(e).findMany(e,{baseWhere:b(n.where,o),relationBaseWhere:y})).page.map(d=>({id:String(d._id),patch:n.patch}));return N(l.length,a?.limit,"patchWhere"),g.patchMany(l,a)},query(e){const{baseWhere:n}=c(e),a=f(e).query(e);return n?a.filter(o=>k(o,n)):a},restore:(e,n)=>w(e,"update",async a=>{const o=a.restore??r.restore;if(!o)throw new p("BAD_REQUEST","restore is not supported by this writer");await o(e,n)},void 0,n),replace:(e,n,a)=>w(e,"update",o=>o.replace(e,n,a),o=>({...n,_creationTime:n._creationTime??o._creationTime,_id:o._id}),a),aggregate(e,n){const{baseWhere:a}=c(e);return f(e).aggregate(e,{...n,baseWhere:b(n.baseWhere,a),relationBaseWhere:y})},groupBy(e,n){const{baseWhere:a}=c(e);return f(e).groupBy(e,{...n,baseWhere:b(n.baseWhere,a),relationBaseWhere:y})},rank(e,n,a){const o=W(e,"rank");return f(e).rank(e,n,{...a,baseWhere:b(a.baseWhere,o)})},rankPage(e,n,a){const o=W(e,"rankPage");return f(e).rankPage(e,n,{...a,baseWhere:b(a?.baseWhere,o)})},wipeShard(){throw new p("FORBIDDEN","ctx.db.wipeShard() is unavailable under rls(): a whole-shard erase can't be policy-gated per row, and erasing only policy-visible rows would silently leave data behind. Run it from an internalMutation, or use ctx.db.deleteAll(table) per table.",{status:403})},...O?{rankBefore:(e,n,a)=>(W(e,"rankBefore"),(f(e).rankBefore??O)(e,n,a))}:{}},A=g;for(const e of s.keys())ne(t[e])&&(A[e]=P(g,e));return g},T=t=>typeof t=="string"?t:t.name,se=t=>{const r=new Map;for(const s of t??[])r.set(s.name,new Set((s.permissions??[]).map(i=>T(i))));return r},ue=(t,r={})=>{const s=$(t),i=se(r.roles);return U(async({ctx:u,next:c})=>{const f=u.auth??{},y=await f.getIdentity?.()??null,v=f.roles??[],B=new Set;for(const n of v)for(const a of i.get(n)??[])B.add(a);const w={auth:{can:n=>B.has(T(n)),identity:y,roles:v,userId:f.userId??null},ctx:u},W=u.db,O=W[C]??W,g=ae(W,O,s,w),A={db:g},{orm:e}=u;return e!==null&&typeof e=="object"&&(A.orm=x(g)),c({ctx:A})},{policies:t,roles:r.roles??[]})};export{q as computeReadBaseWhere,M as evaluateWrite,se as indexRolePermissions,k as matchesWhere,T as permissionName,ue as rls};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as i}from"@lunora/errors";const m=async(a,l,s)=>{let o=-1;const c=async(r,t)=>{if(r<=o)throw new i("INTERNAL","middleware next() called multiple times");o=r;const e=a[r];return e?await e({ctx:t,next:(n=>c(r+1,n?.ctx?{...t,...n.ctx}:t))}):s(t)};return c(0,l)};export{m as l};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import{LunoraError as S}from"./LunoraError-LVhdU0Lo.mjs";const b=t=>typeof t=="string"?t:t.name,v=(t=[])=>{const e=new Map;for(const s of t){const a=new Set;for(const i of s.permissions??[])a.add(b(i));e.set(s.name,a)}return e},N=(t,e)=>t===void 0||e.startsWith(t),R=t=>{const e=t[1],s=typeof e=="object"&&e!==null?e.method:void 0;return typeof s=="string"&&s.toUpperCase()==="PUT"?"write":"read"},$=[["delete","delete"],["download","read"],["generateUploadUrl","write"],["getMetadata","read"],["getSignedUrl",R],["getUrl","read"],["store","write"]],E=(t,e={})=>{const s=v(e.roles);return async({ctx:a,next:i})=>{const l=a.auth??{},h=await l.getIdentity?.()??null,g=l.roles??[],y=new Set;for(const o of g)for(const r of s.get(o)??[])y.add(r);const k={can:o=>y.has(b(o)),identity:h,roles:g,userId:l.userId??null},x=(o,r,c)=>{const f=t.filter(n=>n.on===o&&n.bucket===c);if(f.length===0)return;const u={auth:k,ctx:a,key:r};if(!f.some(n=>N(n.prefix,r)&&n.when(u)===!0))throw new S("FORBIDDEN",`storage ${o} on "${r}" in bucket "${c}" denied by access rule`)},p=o=>{const r=o.bucketName??"default",c={bucketName:r};for(const[u,n]of $){const m=o[u];typeof m=="function"&&(c[u]=(...d)=>{const U=typeof d[0]=="string"?d[0]:"",I=typeof n=="function"?n(d):n;return x(I,U,r),m(...d)})}const{bucket:f}=o;return typeof f=="function"&&(c.bucket=u=>p(f(u))),c},w=a.storage;return w===void 0?i():i({ctx:{storage:p(w)}})}};export{E as storageRules};
|
package/dist/rls/testing.mjs
CHANGED
|
@@ -1,49 +1 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const expectPolicy = (policies, options = {}) => {
|
|
4
|
-
const rolePermissions = indexRolePermissions(options.roles);
|
|
5
|
-
const context = options.ctx ?? {};
|
|
6
|
-
return {
|
|
7
|
-
as: (identity) => {
|
|
8
|
-
const auth = identity ?? {};
|
|
9
|
-
const roles = auth.roles ?? [];
|
|
10
|
-
const granted = /* @__PURE__ */ new Set();
|
|
11
|
-
for (const roleName of roles) {
|
|
12
|
-
for (const name of rolePermissions.get(roleName) ?? []) {
|
|
13
|
-
granted.add(name);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
|
-
const baseContext = {
|
|
17
|
-
auth: {
|
|
18
|
-
can: (permission) => granted.has(permissionName(permission)),
|
|
19
|
-
// eslint-disable-next-line unicorn/no-null -- PolicyContext.auth.identity is a public `… | null` type, mirroring the middleware
|
|
20
|
-
identity: auth.identity ?? null,
|
|
21
|
-
roles,
|
|
22
|
-
// eslint-disable-next-line unicorn/no-null -- PolicyContext.auth.userId is a public `null | string` type, mirroring the middleware
|
|
23
|
-
userId: auth.userId ?? null
|
|
24
|
-
},
|
|
25
|
-
ctx: context
|
|
26
|
-
};
|
|
27
|
-
const can = (op, table, row, nextRow) => {
|
|
28
|
-
const tablePolicies = policies.filter((policy) => policy.table === table);
|
|
29
|
-
if (tablePolicies.length === 0) {
|
|
30
|
-
return true;
|
|
31
|
-
}
|
|
32
|
-
if (op === "read") {
|
|
33
|
-
if (!tablePolicies.some((policy) => policy.on === "read")) {
|
|
34
|
-
return true;
|
|
35
|
-
}
|
|
36
|
-
const baseWhere = computeReadBaseWhere(tablePolicies, baseContext);
|
|
37
|
-
return baseWhere === void 0 || matchesWhere(row ?? {}, baseWhere);
|
|
38
|
-
}
|
|
39
|
-
return evaluateWrite(tablePolicies, op, { ...baseContext, row }, nextRow);
|
|
40
|
-
};
|
|
41
|
-
return {
|
|
42
|
-
can,
|
|
43
|
-
cannot: (op, table, row, nextRow) => !can(op, table, row, nextRow)
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
};
|
|
47
|
-
};
|
|
48
|
-
|
|
49
|
-
export { expectPolicy };
|
|
1
|
+
import{indexRolePermissions as y,computeReadBaseWhere as I,matchesWhere as P,evaluateWrite as W,permissionName as g}from"../packem_shared/rls-_iVsPvhX.mjs";const w=(f,i={})=>{const m=y(i.roles),h=i.ctx??{};return{as:p=>{const s=p??{},c=s.roles??[],u=new Set;for(const e of c)for(const t of m.get(e)??[])u.add(t);const l={auth:{can:e=>u.has(g(e)),identity:s.identity??null,roles:c,userId:s.userId??null},ctx:h},d=(e,t,r,a)=>{const n=f.filter(o=>o.table===t);if(n.length===0)return!0;if(e==="read"){if(!n.some(x=>x.on==="read"))return!0;const o=I(n,l);return o===void 0||P(r??{},o)}return W(n,e,{...l,row:r},a)};return{can:d,cannot:(e,t,r,a)=>!d(e,t,r,a)}}}};export{w as expectPolicy};
|
package/dist/types.d.mts
CHANGED
|
@@ -282,6 +282,17 @@ interface TableDefinition<Shape extends Record<string, Validator> = Record<strin
|
|
|
282
282
|
* Has no effect when the schema does not require RLS.
|
|
283
283
|
*/
|
|
284
284
|
isPublic?: boolean;
|
|
285
|
+
/**
|
|
286
|
+
* Set by `.ownedBy(field)` — the column holding the owning user's id (named
|
|
287
|
+
* `ownerField`, a data field, rather than colliding with the fluent
|
|
288
|
+
* `.ownedBy()` builder method — same convention as `shardBy()`/`shardMode`).
|
|
289
|
+
*
|
|
290
|
+
* A shape over this table with `owner: true` derives its predicate from this
|
|
291
|
+
* field, so "only the owner may replicate these rows" is declared once on the
|
|
292
|
+
* table instead of being restated in every shape's `where`. Absent ⇒ the table
|
|
293
|
+
* has no single owning column and a shape must spell its predicate out.
|
|
294
|
+
*/
|
|
295
|
+
ownerField?: string;
|
|
285
296
|
/**
|
|
286
297
|
* Rank indexes declared via `.rankIndex(name, opts)`. The runtime maintains
|
|
287
298
|
* a sorted companion table per declared rank with a btree on
|
|
@@ -532,6 +543,30 @@ interface SystemDatabaseReader {
|
|
|
532
543
|
* actual SQL implementation lives in `@lunora/do`; these are signatures only.
|
|
533
544
|
*/
|
|
534
545
|
interface DatabaseReader {
|
|
546
|
+
/**
|
|
547
|
+
* The throwing sibling of {@link DatabaseReader.normalizeId}: brand `id` as an
|
|
548
|
+
* {@link Id} for `tableName`, or throw `BAD_REQUEST` when it is not structurally
|
|
549
|
+
* an id. Pure — it never reads the database, so a valid id for a row that
|
|
550
|
+
* doesn't exist still returns.
|
|
551
|
+
*
|
|
552
|
+
* This is the **parse boundary** for an id that arrived as a plain `string`: a
|
|
553
|
+
* wire payload, a mutator's args, a change plan computed on the client. The
|
|
554
|
+
* alternative is `value as Id<"table">` at every such call site — an assertion,
|
|
555
|
+
* not a check, and one that has to be repeated for every table a helper is
|
|
556
|
+
* generic over:
|
|
557
|
+
*
|
|
558
|
+
* ```ts
|
|
559
|
+
* for (const patch of plan.patches) {
|
|
560
|
+
* await ctx.db.patch(ctx.db.asId("nodes", patch.id), patch.fields);
|
|
561
|
+
* }
|
|
562
|
+
* ```
|
|
563
|
+
*
|
|
564
|
+
* Ids are opaque strings, so the check is exactly `normalizeId`'s: it rejects
|
|
565
|
+
* empty, whitespace-bearing, and NUL-bearing values, not "an id that isn't in
|
|
566
|
+
* this table". Use it to get the brand honestly, and `get()` to learn whether
|
|
567
|
+
* the row exists.
|
|
568
|
+
*/
|
|
569
|
+
asId: <T extends string>(tableName: T, id: string) => Id<T>;
|
|
535
570
|
get: <T extends string>(id: Id<T>) => Promise<Record<string, unknown> | null>;
|
|
536
571
|
/**
|
|
537
572
|
* Validate an untrusted `id` string against the structural shape of an id
|
|
@@ -686,6 +721,24 @@ interface InsertManyOptions extends BatchWriteOptions {
|
|
|
686
721
|
}
|
|
687
722
|
interface DatabaseWriter extends DatabaseReader {
|
|
688
723
|
delete: <T extends string>(id: Id<T>) => Promise<void>;
|
|
724
|
+
/**
|
|
725
|
+
* Delete EVERY row in `tableName`, chunking internally until the table is
|
|
726
|
+
* empty — the erasure primitive.
|
|
727
|
+
*
|
|
728
|
+
* Unlike `deleteWhere(tableName, {})` there is **no batch cap**: a
|
|
729
|
+
* `BATCH_LIMIT_EXCEEDED` at row 501 of an account deletion is a bug, not a
|
|
730
|
+
* safety rail. Rows still go through the single-row delete pipeline, so
|
|
731
|
+
* triggers, cascades, companions, CDC, and live subscriptions stay correct.
|
|
732
|
+
*
|
|
733
|
+
* On a `.softDelete()` table the default flips the marker column; pass
|
|
734
|
+
* `{ hard: true }` to remove the rows physically (what GDPR erasure means).
|
|
735
|
+
*/
|
|
736
|
+
deleteAll: (tableName: string, options?: {
|
|
737
|
+
chunkSize?: number;
|
|
738
|
+
hard?: boolean;
|
|
739
|
+
}) => Promise<{
|
|
740
|
+
deleted: number;
|
|
741
|
+
}>;
|
|
689
742
|
/**
|
|
690
743
|
* Delete many rows by id in one call. Each id is deleted through the full
|
|
691
744
|
* single-row pipeline (triggers + per-row RLS). The returned `deleted` is the
|
|
@@ -793,6 +846,29 @@ interface DatabaseWriter extends DatabaseReader {
|
|
|
793
846
|
patched: number;
|
|
794
847
|
}>;
|
|
795
848
|
replace: <T extends string>(id: Id<T>, document: Record<string, unknown>) => Promise<void>;
|
|
849
|
+
/**
|
|
850
|
+
* Erase every shard-local table — the account-deletion / tenant-teardown
|
|
851
|
+
* primitive. Sweeps the schema's non-`.global()` tables with
|
|
852
|
+
* {@link DatabaseWriter.deleteAll}`({ hard: true })` and returns the per-table
|
|
853
|
+
* counts.
|
|
854
|
+
*
|
|
855
|
+
* `.global()` tables are skipped by design: their rows live in D1 and are shared
|
|
856
|
+
* across shards, so "wipe this shard" must not reach them. Restrict the sweep
|
|
857
|
+
* with `options.tables`, or spare one with `options.exclude` (e.g. an audit log
|
|
858
|
+
* that must outlive the data).
|
|
859
|
+
*
|
|
860
|
+
* ```ts
|
|
861
|
+
* export const deleteAccount = internalMutation({ handler: async ({ ctx }) => ctx.db.wipeShard() });
|
|
862
|
+
* ```
|
|
863
|
+
*/
|
|
864
|
+
wipeShard: (options?: {
|
|
865
|
+
chunkSize?: number;
|
|
866
|
+
exclude?: ReadonlyArray<string>;
|
|
867
|
+
tables?: ReadonlyArray<string>;
|
|
868
|
+
}) => Promise<{
|
|
869
|
+
deleted: number;
|
|
870
|
+
tables: Record<string, number>;
|
|
871
|
+
}>;
|
|
796
872
|
}
|
|
797
873
|
/** Authenticated identity surfaced into every context. */
|
|
798
874
|
interface AuthState {
|
|
@@ -1305,6 +1381,28 @@ interface LunoraLogMethod {
|
|
|
1305
1381
|
interface LunoraLogger {
|
|
1306
1382
|
readonly debug: LunoraLogMethod;
|
|
1307
1383
|
readonly error: LunoraLogMethod;
|
|
1384
|
+
/**
|
|
1385
|
+
* Emit a **structured event** instead of a log line — OpenTelemetry's Events
|
|
1386
|
+
* API, on the wire as `LogRecord.eventName` (plus an `event.name` attribute
|
|
1387
|
+
* for collectors predating that field).
|
|
1388
|
+
*
|
|
1389
|
+
* ```ts
|
|
1390
|
+
* ctx.log.event("checkout.completed", { plan: user.plan, total, currency });
|
|
1391
|
+
* ```
|
|
1392
|
+
*
|
|
1393
|
+
* The difference from `ctx.log.info("checkout completed", { … })` is what a
|
|
1394
|
+
* backend can do with it. A log line's payload is its message: prose, written
|
|
1395
|
+
* for a human, free to be reworded next sprint — so "how many checkouts
|
|
1396
|
+
* completed, by plan, this hour" degrades into a substring search over
|
|
1397
|
+
* English. An event's payload is its `fields` under a **stable name**, which a
|
|
1398
|
+
* collector can index, group, and alert on directly.
|
|
1399
|
+
*
|
|
1400
|
+
* Rule of thumb: `log.*` for narration you'd read while debugging, `event` for
|
|
1401
|
+
* anything you'd ever put on a dashboard. And for facts about the request as a
|
|
1402
|
+
* whole, prefer `ctx.span` — one wide event beats a dozen
|
|
1403
|
+
* events, however well named.
|
|
1404
|
+
*/
|
|
1405
|
+
readonly event: (name: string, fields?: LogFields) => void;
|
|
1308
1406
|
readonly fatal: LunoraLogMethod;
|
|
1309
1407
|
readonly info: LunoraLogMethod;
|
|
1310
1408
|
readonly log: LunoraLogMethod;
|
|
@@ -1328,10 +1426,71 @@ interface LunoraLogger {
|
|
|
1328
1426
|
* handle writes are merged over them at record time, post-hoc winning on a clash.
|
|
1329
1427
|
*/
|
|
1330
1428
|
interface SpanHandle {
|
|
1429
|
+
/**
|
|
1430
|
+
* Record a timestamped event on the enclosing span — a retry, a cache miss, a
|
|
1431
|
+
* state transition. Prefer this over an extra `ctx.log` line for anything that
|
|
1432
|
+
* only makes sense *relative to this span*: it rides the span's own export, so
|
|
1433
|
+
* it costs no additional record and can never be separated from its context.
|
|
1434
|
+
*/
|
|
1435
|
+
addEvent: (name: string, attributes?: LogFields) => void;
|
|
1436
|
+
/**
|
|
1437
|
+
* Link this span to one in another trace — how a queue consumer points back at
|
|
1438
|
+
* the request that enqueued its message without collapsing every producer into
|
|
1439
|
+
* one giant trace.
|
|
1440
|
+
*/
|
|
1441
|
+
addLink: (link: SpanLink) => void;
|
|
1442
|
+
/**
|
|
1443
|
+
* Record a **handled** exception as the OTel-conventional `exception` span
|
|
1444
|
+
* event (`exception.type` / `exception.message` / `exception.stacktrace`),
|
|
1445
|
+
* without marking the span failed.
|
|
1446
|
+
*
|
|
1447
|
+
* For an error you swallowed — a retried request, a fallback that worked. An
|
|
1448
|
+
* error that escapes the span body is recorded automatically and *does* set
|
|
1449
|
+
* the error status, so don't call this for one you're re-throwing.
|
|
1450
|
+
*/
|
|
1451
|
+
recordException: (error: unknown) => void;
|
|
1331
1452
|
/** Set one attribute on the enclosing span (merged at record time; post-hoc wins on key clash). */
|
|
1332
1453
|
setAttribute: (key: string, value: LogFields[string]) => void;
|
|
1333
1454
|
/** Merge attributes onto the enclosing span (post-hoc wins on key clash). */
|
|
1334
1455
|
setAttributes: (fields: LogFields) => void;
|
|
1456
|
+
/**
|
|
1457
|
+
* The W3C ids of the span this handle refers to (32-hex trace, 16-hex span).
|
|
1458
|
+
*
|
|
1459
|
+
* On `ctx.span` these are the DISPATCH's ids — the trace the whole request
|
|
1460
|
+
* belongs to. Use it to echo a trace id back to a caller so a user can quote
|
|
1461
|
+
* it in a bug report, to build a `traceparent` for a hand-rolled outbound
|
|
1462
|
+
* call, or to parent a third-party library's spans onto this request.
|
|
1463
|
+
*/
|
|
1464
|
+
spanContext: () => {
|
|
1465
|
+
spanId: string;
|
|
1466
|
+
traceId: string;
|
|
1467
|
+
};
|
|
1468
|
+
}
|
|
1469
|
+
/**
|
|
1470
|
+
* A causal reference to a span in another trace (OTel `Span.links`). Ids are
|
|
1471
|
+
* lowercase hex — 32 chars for `traceId`, 16 for `spanId`.
|
|
1472
|
+
*/
|
|
1473
|
+
interface SpanLink {
|
|
1474
|
+
/** Attributes describing the relationship, e.g. `{ "link.kind": "enqueued_by" }`. */
|
|
1475
|
+
attributes?: LogFields;
|
|
1476
|
+
spanId: string;
|
|
1477
|
+
traceId: string;
|
|
1478
|
+
}
|
|
1479
|
+
/** OTel `SpanKind`. Drives a collector's service map — see {@link SpanOptions.kind}. */
|
|
1480
|
+
type SpanKind = "client" | "consumer" | "internal" | "producer" | "server";
|
|
1481
|
+
/** Options accepted by `ctx.trace(name, fn, options)` beyond a plain attribute bag. */
|
|
1482
|
+
interface SpanOptions {
|
|
1483
|
+
/** Start attributes, snapshotted before the body runs. */
|
|
1484
|
+
attributes?: LogFields;
|
|
1485
|
+
/**
|
|
1486
|
+
* OTel `SpanKind`, default `"internal"`. Set `"client"` for a call OUT to
|
|
1487
|
+
* another service and `"producer"`/`"consumer"` for queue hops: a collector
|
|
1488
|
+
* builds its service map from this, so leaving everything `"internal"` yields
|
|
1489
|
+
* a trace with no topology.
|
|
1490
|
+
*/
|
|
1491
|
+
kind?: SpanKind;
|
|
1492
|
+
/** Links to spans in other traces, known at span start. */
|
|
1493
|
+
links?: SpanLink[];
|
|
1335
1494
|
}
|
|
1336
1495
|
/**
|
|
1337
1496
|
* Span factory on every function `ctx`. Wraps a sub-operation so it becomes its
|
|
@@ -1382,10 +1541,52 @@ interface SpanHandle {
|
|
|
1382
1541
|
* @param fn The body to time, receiving a tracer bound to this span for any
|
|
1383
1542
|
* nested spans and the enclosing span's {@link SpanHandle} for post-hoc
|
|
1384
1543
|
* attributes. May be sync or async; the result is awaited.
|
|
1385
|
-
* @param attributes
|
|
1386
|
-
* normalized like a log line's `fields
|
|
1544
|
+
* @param attributes Either a plain attribute bag to stamp on the span at start
|
|
1545
|
+
* (normalized like a log line's `fields`), or a {@link SpanOptions} object when
|
|
1546
|
+
* you need `kind` or `links`. It is read as options only when *every* key is one
|
|
1547
|
+
* of `attributes`/`kind`/`links`; `{ attributes: { kind: "premium" } }` is the
|
|
1548
|
+
* explicit form if your own attributes happen to be named that.
|
|
1549
|
+
*/
|
|
1550
|
+
type LunoraTracer = <T>(name: string, function_: (trace: LunoraTracer, span: SpanHandle) => Promise<T> | T, attributes?: LogFields | SpanOptions) => Promise<T>;
|
|
1551
|
+
/**
|
|
1552
|
+
* `ctx.span` — a handle onto **this request's own span**, and with it the
|
|
1553
|
+
* wide-event API.
|
|
1554
|
+
*
|
|
1555
|
+
* ```ts
|
|
1556
|
+
* export const checkout = mutation({ handler: async (ctx, args) => {
|
|
1557
|
+
* ctx.span.setAttributes({ "user.plan": user.plan, "cart.items": cart.length });
|
|
1558
|
+
* const payment = await charge(cart);
|
|
1559
|
+
* ctx.span.setAttributes({ "payment.provider": payment.provider, "payment.total": payment.total });
|
|
1560
|
+
* if (payment.retried) ctx.span.addEvent("payment.retried", { attempts: payment.attempts });
|
|
1561
|
+
* return payment;
|
|
1562
|
+
* }});
|
|
1563
|
+
* ```
|
|
1564
|
+
*
|
|
1565
|
+
* **Why this instead of more log lines.** The usual way to make a handler
|
|
1566
|
+
* observable is to sprinkle `ctx.log.info` through it, which costs one record per
|
|
1567
|
+
* call, scatters one request's facts across a dozen rows, and forces every
|
|
1568
|
+
* question to be answered by correlating them back together. The wide-event
|
|
1569
|
+
* pattern inverts that: accumulate the facts as you learn them, and emit **one**
|
|
1570
|
+
* richly-attributed record per unit of work. Cost is flat — one span per request
|
|
1571
|
+
* no matter how much you attach — and every question ("p99 checkout latency for
|
|
1572
|
+
* pro-plan users with >10 items") becomes a single filter over one table instead
|
|
1573
|
+
* of a join across log lines.
|
|
1574
|
+
*
|
|
1575
|
+
* **This is plain OpenTelemetry, not a Lunora convention.** The attributes land
|
|
1576
|
+
* on the span the dispatch already emits, and are additionally exported as an
|
|
1577
|
+
* OTel Event record named `lunora.dispatch`, correlated by `trace_id`/`span_id`.
|
|
1578
|
+
* Any OTLP backend groups and aggregates them with no special configuration.
|
|
1579
|
+
*
|
|
1580
|
+
* **`span` vs `trace`.** `ctx.trace(name, fn)` creates a NEW child span to time a
|
|
1581
|
+
* sub-operation; `ctx.span` attaches to the one that already exists for the
|
|
1582
|
+
* request. Use `trace` for "how long did this part take", `span` for "what was
|
|
1583
|
+
* true about this request". Inside a `ctx.trace` body, the handle passed as the
|
|
1584
|
+
* body's second argument is that child span's equivalent of this.
|
|
1585
|
+
*
|
|
1586
|
+
* Attributes are normalized exactly like `ctx.log` fields, and recording is
|
|
1587
|
+
* best-effort — a telemetry failure never breaks the handler.
|
|
1387
1588
|
*/
|
|
1388
|
-
type
|
|
1589
|
+
type LunoraWideEvent = SpanHandle;
|
|
1389
1590
|
/**
|
|
1390
1591
|
* Application metrics on every function `ctx` — the third signal alongside
|
|
1391
1592
|
* `ctx.log` and `ctx.trace`. Each call records one measurement that flows to an
|
|
@@ -1470,6 +1671,8 @@ interface QueryCtx {
|
|
|
1470
1671
|
readonly runQuery: <A extends ArgsValidator, R>(reference: RegisteredQuery<A, R>, args: InferArgs<A>) => Promise<R>;
|
|
1471
1672
|
/** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
|
|
1472
1673
|
readonly secrets: Secrets;
|
|
1674
|
+
/** Attach facts to THIS request's span — the wide event; see {@link LunoraWideEvent}. */
|
|
1675
|
+
readonly span: LunoraWideEvent;
|
|
1473
1676
|
readonly storage: ReadOnlyStorage;
|
|
1474
1677
|
/** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
|
|
1475
1678
|
readonly trace: LunoraTracer;
|
|
@@ -1524,6 +1727,8 @@ interface MutationCtx {
|
|
|
1524
1727
|
readonly scheduler: Scheduler;
|
|
1525
1728
|
/** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
|
|
1526
1729
|
readonly secrets: Secrets;
|
|
1730
|
+
/** Attach facts to THIS request's span — the wide event; see {@link LunoraWideEvent}. */
|
|
1731
|
+
readonly span: LunoraWideEvent;
|
|
1527
1732
|
readonly storage: ReadOnlyStorage;
|
|
1528
1733
|
/** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
|
|
1529
1734
|
readonly trace: LunoraTracer;
|
|
@@ -1573,6 +1778,8 @@ interface ActionCtx {
|
|
|
1573
1778
|
readonly scheduler: Scheduler;
|
|
1574
1779
|
/** Read account-level secrets from Cloudflare Secrets Store; see {@link Secrets}. */
|
|
1575
1780
|
readonly secrets: Secrets;
|
|
1781
|
+
/** Attach facts to THIS request's span — the wide event; see {@link LunoraWideEvent}. */
|
|
1782
|
+
readonly span: LunoraWideEvent;
|
|
1576
1783
|
readonly storage: Storage;
|
|
1577
1784
|
/** Wrap a sub-operation in its own nested span; see {@link LunoraTracer}. */
|
|
1578
1785
|
readonly trace: LunoraTracer;
|
|
@@ -1586,4 +1793,4 @@ interface ActionCtx {
|
|
|
1586
1793
|
*/
|
|
1587
1794
|
type AnyApi = Record<string, Record<string, RegisteredFunction<ArgsValidator, unknown, FunctionKind>>>;
|
|
1588
1795
|
declare const anyApi: AnyApi;
|
|
1589
|
-
export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExposeConfig, type ExternalSourceCursor, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GeoBoundingBox, type GeoFilterBuilder, type GeoIndexDefinition, type GeoPointInput, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LogFields, type LunoraLogMethod, type LunoraLogger, type LunoraMetrics, type LunoraTracer, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type SpanHandle, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type TtlDefinition, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, type X402ProcedureConfig, anyApi };
|
|
1796
|
+
export { type ActionCtx, type AggregateIndexDefinition, type AggregateOp, type AnyApi, type ArgsValidator, type AuthState, type CachePurge, type DatabaseReader, type DatabaseWriter, type DurableObjectJurisdiction, type ExposeConfig, type ExternalSourceCursor, type ExternalSourceDefinition, type ExternalSourceMode, type ExternalSourceRefresh, type FunctionKind, type FunctionVisibility, type GeoBoundingBox, type GeoFilterBuilder, type GeoIndexDefinition, type GeoPointInput, type GlobalBackend, type IndexDefinition, type IndexRangeBuilder, type InferArgs, type LifecycleEvent, type LifecycleEventKind, type LogFields, type LunoraLogMethod, type LunoraLogger, type LunoraMetrics, type LunoraTracer, type LunoraWideEvent, type MutationCtx, type OnDeleteAction, type PaginationOptions, type PaginationResult, type QueryCtx, type RankIndexDefinition, type RankSortKey, type ReadOnlyStorage, type RegisteredAction, type RegisteredFunction, type RegisteredLifecycleHook, type RegisteredMutation, type RegisteredQuery, type RegisteredStream, type RelationDefinition, type ScheduledFunctionDoc, type ScheduledJob, type Scheduler, type Schema, type SearchFilterBuilder, type SearchIndexDefinition, type Secrets, type SecretsStoreSecretLike, type ShardMode, type SpanHandle, type SpanKind, type SpanLink, type SpanOptions, type Storage, type StorageMetadata, type SystemDatabaseReader, type SystemDoc, type SystemQuery, type SystemTableName, type TableDefinition, type TableReader, type TableVectorIndex, type TriggerAggregateOptions, type TriggerBuilder, type TriggerCtx, type TriggerDatabase, type TriggerDefinition, type TriggerDeleteEvent, type TriggerEvent, type TriggerGroupByEntry, type TriggerGroupByOptions, type TriggerHandler, type TriggerInsertEvent, type TriggerOp, type TriggerQueryArgs, type TriggerQueryPage, type TriggerRankOptions, type TriggerRankPageOptions, type TriggerRankResult, type TriggerRow, type TriggerTiming, type TriggerUpdateEvent, type TtlDefinition, type VectorEmbedder, type VectorIndexDefinition, type VectorMatch, type VectorMatches, type VectorMetric, type VectorQueryInput, type VectorRecord, type VectorSearch, type VectorSearchReader, type VectorUpsertInput, type WorkflowCreateOptions, type WorkflowHandle, type WorkflowInstance, type WorkflowInstanceStatus, type WorkflowStatusResult, type Workflows, type X402ProcedureConfig, anyApi };
|