@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.
Files changed (65) hide show
  1. package/LICENSE.md +213 -0
  2. package/dist/data-model.mjs +0 -1
  3. package/dist/drizzle.mjs +1 -1
  4. package/dist/index.d.mts +106 -9
  5. package/dist/index.d.ts +106 -9
  6. package/dist/index.mjs +1 -29
  7. package/dist/otel.d.mts +543 -0
  8. package/dist/otel.d.ts +543 -0
  9. package/dist/otel.mjs +1 -0
  10. package/dist/packem_shared/LunoraEnvError-CgpI2Mm_.mjs +3 -0
  11. package/dist/packem_shared/LunoraError-LVhdU0Lo.mjs +1 -0
  12. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-CF_9uiu1.mjs +1 -0
  13. package/dist/packem_shared/allowAll-BnyNbJZT.mjs +1 -0
  14. package/dist/packem_shared/asBucketStorage-1pFfH-Tn.mjs +1 -0
  15. package/dist/packem_shared/bindOrm-Bp9hsM2q.mjs +1 -0
  16. package/dist/packem_shared/buildRlsReadRegistry-WdiqSj87.mjs +1 -0
  17. package/dist/packem_shared/composePluginMiddleware-COr09CXA.mjs +1 -0
  18. package/dist/packem_shared/createPolicyDsl-sV1swpkD.mjs +1 -0
  19. package/dist/packem_shared/createSecrets-CgVPiW2C.mjs +1 -0
  20. package/dist/packem_shared/defineAggregateIndex-CYuH6IbJ.mjs +1 -0
  21. package/dist/packem_shared/defineIdentity-B7gfAgxx.mjs +1 -0
  22. package/dist/packem_shared/defineMigration-Bfpwxv2f.mjs +1 -0
  23. package/dist/packem_shared/defineMutator-8Kz0j1Kt.mjs +1 -0
  24. package/dist/packem_shared/defineShape-CyuRfwHz.mjs +1 -0
  25. package/dist/packem_shared/defineStorageRule-BDu01PUn.mjs +1 -0
  26. package/dist/packem_shared/functions-CDC08CWY.mjs +1 -0
  27. package/dist/packem_shared/httpAction-C14NuF3V.mjs +4 -0
  28. package/dist/packem_shared/initLunora-CHh1EmR4.mjs +1 -0
  29. package/dist/packem_shared/mask-C6Bi78qj.mjs +1 -0
  30. package/dist/packem_shared/onConnect-CEtRmUpJ.mjs +1 -0
  31. package/dist/packem_shared/policy-tag-Dprt9JWo.mjs +1 -0
  32. package/dist/packem_shared/protectPublic-BhKewPqm.mjs +1 -0
  33. package/dist/packem_shared/rls-_iVsPvhX.mjs +1 -0
  34. package/dist/packem_shared/run-middleware-BeEEqmdE.mjs +1 -0
  35. package/dist/packem_shared/storageRules-BptPZbi8.mjs +1 -0
  36. package/dist/rls/testing.mjs +1 -49
  37. package/dist/types.d.mts +211 -4
  38. package/dist/types.d.ts +211 -4
  39. package/dist/types.mjs +1 -31
  40. package/package.json +13 -1
  41. package/dist/packem_shared/LunoraEnvError-BGmd1Qs0.mjs +0 -187
  42. package/dist/packem_shared/LunoraError-WbxmrpxR.mjs +0 -9
  43. package/dist/packem_shared/PRESENCE_DEFAULT_TTL_MS-Cr0i4mTv.mjs +0 -114
  44. package/dist/packem_shared/asBucketStorage-Cnxd9y2q.mjs +0 -11
  45. package/dist/packem_shared/bindOrm-CaY7Wq9Z.mjs +0 -147
  46. package/dist/packem_shared/buildRlsReadRegistry-2uk_GfiH.mjs +0 -107
  47. package/dist/packem_shared/composePluginMiddleware-z62dttBo.mjs +0 -103
  48. package/dist/packem_shared/createPolicyDsl-By3QB4he.mjs +0 -32
  49. package/dist/packem_shared/createSecrets-DwaR2rNG.mjs +0 -58
  50. package/dist/packem_shared/defineAggregateIndex-cdo0g-un.mjs +0 -349
  51. package/dist/packem_shared/defineIdentity-DiX4zM9x.mjs +0 -35
  52. package/dist/packem_shared/defineMigration-Hx01yIht.mjs +0 -10
  53. package/dist/packem_shared/defineMutator-EIXAWhs9.mjs +0 -11
  54. package/dist/packem_shared/defineShape-C5scNOrf.mjs +0 -18
  55. package/dist/packem_shared/defineStorageRule-B5nL4Z1P.mjs +0 -23
  56. package/dist/packem_shared/functions-Di9FUNkf.mjs +0 -5
  57. package/dist/packem_shared/httpAction-DCXoYPIk.mjs +0 -355
  58. package/dist/packem_shared/initLunora-D0Wuki7S.mjs +0 -115
  59. package/dist/packem_shared/mask-BepaW7YN.mjs +0 -334
  60. package/dist/packem_shared/onConnect-CIPXKPyw.mjs +0 -13
  61. package/dist/packem_shared/policy-tag-DvpVH2tv.mjs +0 -13
  62. package/dist/packem_shared/protectPublic-BlcGpiRc.mjs +0 -15
  63. package/dist/packem_shared/rls-BaDQf7MG.mjs +0 -595
  64. package/dist/packem_shared/run-middleware-I6EiQfxL.mjs +0 -20
  65. 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};
@@ -1,49 +1 @@
1
- import { indexRolePermissions, computeReadBaseWhere, matchesWhere, evaluateWrite, permissionName } from '../packem_shared/rls-BaDQf7MG.mjs';
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&lt;"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 Structured attributes to stamp on the span at start,
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 LunoraTracer = <T>(name: string, function_: (trace: LunoraTracer, span: SpanHandle) => Promise<T> | T, attributes?: LogFields) => Promise<T>;
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 };