@stacksjs/security 0.75.568 → 0.75.571

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/hash.d.ts CHANGED
@@ -23,6 +23,18 @@ export declare function needsRehash(hash: string, options?: HashMakeOptions): bo
23
23
  * Similar to Laravel's Hash::make()
24
24
  */
25
25
  export declare function make(value: string, options?: HashMakeOptions): Promise<string>;
26
+ /**
27
+ * `make`, answering in place.
28
+ *
29
+ * A model write returns its row where it is written, and a `set:` hook that
30
+ * hashes a password is part of that write: if the hook is asynchronous the
31
+ * write has to wait for it and becomes a promise. This is the same hash,
32
+ * computed on the calling thread. That costs what the hash costs (about a
33
+ * quarter of a second for bcrypt at 12 rounds) and nothing else runs on the
34
+ * thread meanwhile, so use it where the row is wanted in place and `make`
35
+ * where the wait is the better trade.
36
+ */
37
+ export declare function makeSync(value: string, options?: HashMakeOptions): string;
26
38
  /**
27
39
  * Verify a value against a hash
28
40
  * Similar to Laravel's Hash::check()
@@ -84,10 +96,11 @@ export type HashAlgorithm = 'bcrypt' | 'argon2' | 'argon2id' | 'argon2i' | 'argo
84
96
  // Function-based exports (preferred API)
85
97
  export {
86
98
  make as hashMake,
99
+ makeSync as hashMakeSync,
87
100
  check as hashCheck,
88
101
  needsRehash as hashNeedsRehash,
89
102
  info as hashInfo,
90
103
  detectAlgorithm as hashDetectAlgorithm,
91
104
  };
92
105
  // Legacy exports for backwards compatibility
93
- export { make as makeHash, check as verifyHash, base64Encode };
106
+ export { make as makeHash, makeSync as makeHashSync, check as verifyHash, base64Encode };
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // @bun
2
- var se=import.meta.require;import{Buffer as y}from"buffer";import{config as M}from"@stacksjs/config";import{decrypt as K,encrypt as Ge}from"ts-security-crypto";function O(t){return t.startsWith("base64:")?t.slice(7):t}var L=1,I=2,oe="stacks:encrypt:v2",H=600000,B=32,m=16,f=12,ie=64,p=new Map;function ae(t,e,r){return`${r}:${y.from(e).toString("base64")}:${t}`}async function F(t,e,r){let n=ae(t,e,r),s=p.get(n);if(s)return s.hits++,p.delete(n),p.set(n,s),s.key;let o=await crypto.subtle.importKey("raw",new TextEncoder().encode(t),{name:"PBKDF2"},!1,["deriveKey"]),i=await crypto.subtle.deriveKey({name:"PBKDF2",salt:new Uint8Array(e),iterations:r,hash:"SHA-256"},o,{name:"AES-GCM",length:B*8},!1,["encrypt","decrypt"]);if(p.set(n,{key:i,hits:1}),p.size>ie){let a=p.keys().next().value;if(a!==void 0)p.delete(a)}return i}function _(t){if(!t.startsWith("base64:"))return null;let e=y.from(t.slice(7),"base64");return e.length>=B?new Uint8Array(e):null}async function V(t,e){let r=await crypto.subtle.importKey("raw",new Uint8Array(t),"HKDF",!1,["deriveKey"]);return await crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(e),info:new TextEncoder().encode(oe)},r,{name:"AES-GCM",length:B*8},!1,["encrypt","decrypt"])}async function ce(t,e){let r=crypto.getRandomValues(new Uint8Array(m)),n=crypto.getRandomValues(new Uint8Array(f)),s=await V(e,r),o=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:n},s,new TextEncoder().encode(t))),i=new Uint8Array(1+m+f+o.byteLength);return i[0]=I,i.set(r,1),i.set(n,1+m),i.set(o,1+m+f),y.from(i).toString("base64")}async function ue(t,e){let r=y.from(t,"base64");if(r.length<1+m+f+16)return null;if(r[0]!==I)return null;let n=new Uint8Array(r.subarray(1,1+m)),s=new Uint8Array(r.subarray(1+m,1+m+f)),o=new Uint8Array(r.subarray(1+m+f)),i=await V(e,n);try{return new TextDecoder().decode(await crypto.subtle.decrypt({name:"AES-GCM",iv:s},i,o))}catch{return null}}async function me(t,e){let r=crypto.getRandomValues(new Uint8Array(m)),n=crypto.getRandomValues(new Uint8Array(f)),s=await F(e,r,H),o=await crypto.subtle.encrypt({name:"AES-GCM",iv:n},s,new TextEncoder().encode(t)),i=new Uint8Array(o),a=new Uint8Array(1+m+f+i.byteLength);return a[0]=L,a.set(r,1),a.set(n,1+m),a.set(i,1+m+f),y.from(a).toString("base64")}async function le(t,e){let r=y.from(t,"base64");if(r.length<1+m+f+16)return null;if(r[0]!==L)return null;let n=new Uint8Array(r.subarray(1,1+m)),s=new Uint8Array(r.subarray(1+m,1+m+f)),o=new Uint8Array(r.subarray(1+m+f)),i=await F(e,n,H);try{let a=await crypto.subtle.decrypt({name:"AES-GCM",iv:s},i,o);return new TextDecoder().decode(a)}catch{return null}}async function De(t,e){if(!t&&t!=="")throw Error("encrypt() requires a string message");let r=e||M.app.key;if(!r)throw Error("APP_KEY is not defined");let n=_(r);if(n)return await ce(t,n);return await me(t,O(r))}async function qe(t,e){if(!t)throw Error("decrypt() requires a non-empty encrypted string");let r=e||M.app.key;if(!r)throw Error("APP_KEY is not defined");let n=O(r),s=_(r);if(s){let i=await ue(t,s);if(i!==null)return i}let o=await le(t,n);if(o!==null)return o;try{return await K(t,n)}catch(i){if(n!==r)try{return await K(t,r)}catch{}throw i}}import{timingSafeEqual as G}from"crypto";import{base64Encode as Je,hashPassword as q,verifyPassword as D}from"ts-security-crypto";var w=null;function R(){if(w)return w;try{let{hashing:t}=se("@stacksjs/config");return w=t||{},w}catch{return console.warn("[Security] Failed to load hashing config, using defaults"),w={driver:"bcrypt",bcrypt:{rounds:12},argon2:{memory:65536,time:2}},w}}function W(t){if(!t||typeof t!=="string")return"unknown";if(t.startsWith("$2a$")||t.startsWith("$2b$")||t.startsWith("$2y$"))return"bcrypt";if(t.startsWith("$argon2id$"))return"argon2id";if(t.startsWith("$argon2i$"))return"argon2i";if(t.startsWith("$argon2d$"))return"argon2d";return"unknown"}function fe(t){let e=W(t),r={};if(e==="bcrypt"){let n=t.match(/^\$2[aby]\$(\d{2})\$/);if(n&&n[1]!==void 0)r.rounds=Number.parseInt(n[1],10)}else if(e==="argon2id"||e==="argon2i"||e==="argon2d"){let n=t.match(/v=(\d+)/),s=t.match(/m=(\d+)/),o=t.match(/t=(\d+)/),i=t.match(/p=(\d+)/);if(n&&n[1]!==void 0)r.version=Number.parseInt(n[1],10);if(s&&s[1]!==void 0)r.memory=Number.parseInt(s[1],10);if(o&&o[1]!==void 0)r.rounds=Number.parseInt(o[1],10);if(i&&i[1]!==void 0)r.parallelism=Number.parseInt(i[1],10)}return{algorithm:e,options:r}}function Xe(t,e){let r=fe(t),n=R(),s=e?.algorithm||n.driver||"bcrypt",o=(c)=>{if(c==="argon2")return"argon2id";return c},i=o(r.algorithm),a=o(s);if(i!==a)return!0;if(i==="bcrypt"){let c=e?.rounds||n.bcrypt?.rounds||12;if(r.options.rounds!==c)return!0}if(i.startsWith("argon2")){let c=e?.memory||n.argon2?.memory||65536,u=e?.time||n.argon2?.time||2;if(r.options.memory!==c||r.options.rounds!==u)return!0}return!1}async function je(t,e){let r=R(),n=e?.algorithm||r.driver||"bcrypt";if(n==="bcrypt")return await ge(t,e?.rounds);if(n==="argon2"||n==="argon2id"||n==="argon2i"||n==="argon2d")return await he(t,{type:n==="argon2"?"argon2id":n,memory:e?.memory,time:e?.time});throw Error(`Unsupported hashing algorithm: ${n}`)}async function Qe(t,e){if(!t||!e)return!1;if(W(e)==="unknown")try{return await D(t,e)}catch{return!1}return await D(t,e)}async function ge(t,e){let r=R(),n=e||r.bcrypt?.rounds||12;return await q(t,{algorithm:"bcrypt",cost:n})}async function he(t,e){let r=R(),n=e?.type||"argon2id",s=e?.memory||r.argon2?.memory||65536,o=e?.time||r.argon2?.time||2;return await q(t,{algorithm:n,memoryCost:s,timeCost:o})}function Ze(t,e){let r=Buffer.from(t),n=Buffer.from(e);if(r.length!==n.length)return G(r,r),!1;return G(r,n)}import{generateKey as de}from"ts-security-crypto";function rt(){return de(32)}import{createHmac as pe,randomBytes as ye,timingSafeEqual as z}from"crypto";class l extends Error{reason;constructor(t,e="mismatch"){super(t);this.name="InvalidWebhookSignature",this.reason=e}}function k(t){if(!/^[0-9a-f]*$/i.test(t)||t.length%2!==0)throw new l("Signature is not valid hex","malformed");return Buffer.from(t,"hex")}function C(t,e){if(t.length!==e.length)return z(t,t),!1;return z(t,e)}function S(t,e,r){return pe(r,t).update(e).digest()}function we(t,e){let r=t.split(","),n=Number.NaN,s=[];for(let o of r){let i=o.indexOf("=");if(i===-1)continue;let a=o.slice(0,i).trim(),c=o.slice(i+1).trim();if(a==="t")n=Number.parseInt(c,10);else if(a==="v1")s.push(c)}if(!Number.isFinite(n)||s.length===0)throw new l(`${e} header is malformed`,"malformed");return{timestamp:n,signatures:s}}function Y(t,e,r,n,s){if(!t)throw new l(`${s.name} secret is empty`,"missing");if(!e)throw new l(`${s.header} header is missing`,"missing");let{timestamp:o,signatures:i}=we(e,s.header),a=n.toleranceSeconds??300,c=Math.floor((n.now??Date.now())/1000);if(Math.abs(c-o)>a)throw new l(`${s.name} timestamp is outside tolerance window`,"expired");let u=typeof r==="string"?r:r.toString("utf8"),g=S(t,`${o}.${u}`,"sha256");for(let b of i){let x;try{x=k(b)}catch{continue}if(C(g,x))return!0}throw new l(`${s.name} signature did not match`,"mismatch")}function be(t,e,r,n={}){return Y(t,e,r,n,{name:"Stripe webhook",header:"Stripe-Signature"})}function xe(t,e,r,n={}){return Y(t,e,r,n,{name:"Webhook",header:"Signature"})}function ot(t,e,r={}){let n=(Array.isArray(t)?t:[t]).filter((a)=>typeof a==="string"&&a.length>0);if(!n.length)throw new l("Webhook signing secret is empty","missing");let s=r.timestamp??Math.floor(Date.now()/1000);if(!Number.isInteger(s)||s<0)throw new l("Webhook timestamp must be whole unix seconds","malformed");let o=typeof e==="string"?e:e.toString("utf8"),i=n.map((a)=>S(a,`${s}.${o}`,"sha256").toString("hex"));return{timestamp:s,signatures:i,header:[`t=${s}`,...i.map((a)=>`v1=${a}`)].join(",")}}function it(t="whsec_"){return`${t}${ye(32).toString("base64url")}`}function ve(t,e,r){if(!t)throw new l("GitHub webhook secret is empty","missing");if(!e)throw new l("X-Hub-Signature-256 header is missing","missing");let n="sha256=";if(!e.startsWith(n))throw new l("X-Hub-Signature-256 has wrong algorithm prefix","malformed");let s=k(e.slice(n.length)),o=S(t,r,"sha256");if(!C(o,s))throw new l("GitHub webhook signature did not match","mismatch");return!0}function Ae(t,e,r,n="sha256"){if(!t)throw new l("HMAC secret is empty","missing");if(!r)throw new l("Signature header is missing","missing");let s=r.indexOf("="),o=s!==-1&&/^[a-z0-9]+$/i.test(r.slice(0,s))?r.slice(s+1):r,i=k(o),a=S(t,e,n);if(!C(a,i))throw new l("HMAC signature did not match","mismatch");return!0}function at(t,e,r,n,s={}){switch(t){case"stripe":return be(e,r,n,s);case"github":return ve(e,r,n);case"generic":return Ae(e,n,r,s.algorithm??"sha256");case"signed":return xe(e,r,n,s);default:throw new l(`Unknown webhook provider: ${String(t)}`,"malformed")}}import{createHash as Re,randomBytes as $e}from"crypto";function Se(t){return Re("sha256").update(t,"utf8").digest("hex")}function Ee(t,e=""){let r=e&&t.startsWith(e)?t.slice(e.length):t;if(r.length<=8)return`${e}\u2026`;return`${e}${r.slice(0,4)}\u2026${r.slice(-4)}`}function mt(t="",e=32){if(!Number.isInteger(e)||e<16)throw Error("An API key needs at least 16 random bytes");let r=`${t}${$e(e).toString("base64url")}`;return{key:r,hash:Se(r),hint:Ee(r,t)}}import{lookup as Ue}from"dns/promises";import{isIP as U}from"net";class d extends Error{reason;constructor(t,e){super(t);this.name="OutboundUrlError",this.reason=e}}function X(t){let e=t.split(".");if(e.length!==4)return null;let r=e.map((n)=>/^\d{1,3}$/.test(n)?Number(n):Number.NaN);return r.every((n)=>Number.isInteger(n)&&n>=0&&n<=255)?r:null}function E(t){let e=X(t);if(!e)return!0;let[r,n,s]=e;return r===0||r===10||r===100&&n>=64&&n<=127||r===127||r===169&&n===254||r===172&&n>=16&&n<=31||r===192&&n===0&&(s===0||s===2)||r===192&&n===88&&s===99||r===192&&n===168||r===198&&(n===18||n===19)||r===198&&n===51&&s===100||r===203&&n===0&&s===113||r>=224}function Pe(t){let e=t.toLowerCase(),r=e.indexOf("%");if(r!==-1)e=e.slice(0,r);let n=e.match(/^(.*:)(\d+\.\d+\.\d+\.\d+)$/);if(n){let u=X(n[2]);if(!u)return null;e=`${n[1]}${(u[0]<<8|u[1]).toString(16)}:${(u[2]<<8|u[3]).toString(16)}`}let s=e.split("::");if(s.length>2)return null;let o=s[0]?s[0].split(":"):[],i=s.length===2&&s[1]?s[1].split(":"):[],a=8-o.length-i.length;if(s.length===1&&a!==0||a<0)return null;let c=[...o,...Array.from({length:s.length===2?a:0},()=>"0"),...i];if(c.length!==8||c.some((u)=>!/^[0-9a-f]{1,4}$/.test(u)))return null;return c.map((u)=>Number.parseInt(u,16))}function Te(t){let e=Pe(t);if(!e)return!0;let r=`${e[6]>>8}.${e[6]&255}.${e[7]>>8}.${e[7]&255}`;if(e.slice(0,7).every((n)=>n===0))return!0;if(e.slice(0,5).every((n)=>n===0)&&e[5]===65535)return E(r);if(e[0]===100&&e[1]===65435&&e.slice(2,6).every((n)=>n===0))return E(r);if(e[0]===8194)return E(`${e[1]>>8}.${e[1]&255}.${e[2]>>8}.${e[2]&255}`);return(e[0]&65024)===64512||(e[0]&65472)===65152||(e[0]&65280)===65280||e[0]===8193&&e[1]===3512||e[0]===8193&&e[1]<512||e[0]===256}function j(t){let e=t.replace(/^\[|\]$/g,""),r=U(e.split("%")[0]);if(r===4)return E(e);if(r===6)return Te(e);return!0}var Be=[".localhost",".local",".internal",".localdomain",".home.arpa"];function ke(t,e={}){let r;try{r=new URL(t)}catch{return{reason:"invalid",message:"Not a valid URL"}}if(!(e.allowHttp?["https:","http:"]:["https:"]).includes(r.protocol))return{reason:"scheme",message:e.allowHttp?"The URL must start with https:// or http://":"The URL must start with https://"};if(r.username||r.password)return{reason:"credentials",message:"The URL must not contain a username or password"};if(e.allowPrivate)return null;let s=r.hostname.replace(/^\[|\]$/g,"").toLowerCase().replace(/\.$/,"");if(!s)return{reason:"invalid",message:"Not a valid URL"};if(U(s.split("%")[0]))return j(s)?{reason:"private",message:"The URL points at a private or local address"}:null;if(s==="localhost"||!s.includes(".")||Be.some((o)=>s.endsWith(o)))return{reason:"private",message:"The URL points at a private or local address"};return null}async function Ce(t){return(await Ue(t,{all:!0,verbatim:!0})).map((r)=>r.address)}async function ht(t,e={}){return(await Q(t,e)).parsed}async function Q(t,e){let r=ke(t,e);if(r)throw new d(r.message,r.reason);let n=new URL(t),s=n.hostname.replace(/^\[|\]$/g,"");if(e.allowPrivate||U(s.split("%")[0]))return{parsed:n,addresses:[]};let o;try{o=await(e.resolve??Ce)(s)}catch{throw new d(`Could not resolve ${s}`,"unresolvable")}if(!o.length)throw new d(`Could not resolve ${s}`,"unresolvable");if(o.some(j))throw new d("The URL points at a private or local address","private");return{parsed:n,addresses:o}}var Ne=new Set([301,302,303,307,308]),Ke=["authorization","cookie","proxy-authorization"];function Me(t,e,r){if(!e)return[t.href,r];let n=new URL(t.href);n.hostname=U(e)===6?`[${e}]`:e;let s=new Headers(r.headers);s.set("Host",t.host);let o={...r,headers:s};if(t.protocol==="https:")o.tls={serverName:t.hostname.replace(/^\[|\]$/g,"")};return[n.href,o]}async function Z(t){await t.body?.cancel().catch(()=>{})}async function Oe(t,e){e.throwIfAborted();let r=()=>{},n=new Promise((s,o)=>{r=()=>o(e.reason),e.addEventListener("abort",r,{once:!0})});try{return await Promise.race([Promise.resolve().then(t),n])}finally{e.removeEventListener("abort",r)}}async function J(t,e){let r=Number(t.headers.get("content-length")??Number.NaN);if(Number.isFinite(r)&&r>e)throw await Z(t),new d(`The response is ${r} bytes, over the ${e} byte limit`,"too-large");if(!t.body)return t;let n=[],s=0,o=t.body.getReader();try{for(;;){let{done:c,value:u}=await o.read();if(c)break;if(s+=u.byteLength,s>e)throw await o.cancel(),new d(`The response is over the ${e} byte limit`,"too-large");n.push(u)}}finally{o.releaseLock()}let i=new Uint8Array(s),a=0;for(let c of n)i.set(c,a),a+=c.byteLength;return new Response(i,{status:t.status,statusText:t.statusText,headers:t.headers})}async function dt(t,e={},r={}){let n=r.maxRedirects??3,s=r.maxBytes??5242880,o=r.timeoutMs??1e4,i=AbortSignal.timeout(o),a=e.signal?AbortSignal.any([e.signal,i]):i,c=e.body==null||typeof e.body==="string"||e.body instanceof Uint8Array||e.body instanceof ArrayBuffer,u=t,g={...e,signal:a,redirect:"manual"},b="";for(let x=0;;x++){a.throwIfAborted();let{parsed:A,addresses:te}=await Oe(()=>Q(u,r),a);if(b&&A.origin!==b){let v=new Headers(g.headers);for(let T of Ke)v.delete(T);g={...g,headers:v}}b=A.origin;let[re,ne]=Me(A,te[0],g),h=await fetch(re,ne);if(!Ne.has(h.status))return await J(h,s);let N=h.headers.get("location");if(!N)return await J(h,s);if(await Z(h),x>=n)throw new d(`The URL redirected more than ${n} times`,"redirects");if(h.status!==303&&g.body!=null&&!c)throw new d("The URL redirected, and a streaming request body cannot be sent again","unreplayable-body");let P=(g.method??"GET").toUpperCase();if((h.status===301||h.status===302)&&P==="POST"||h.status===303&&P!=="GET"&&P!=="HEAD"){let v=new Headers(g.headers);for(let T of["content-encoding","content-language","content-location","content-type","content-length"])v.delete(T);g={...g,headers:v,method:"GET",body:void 0}}u=new URL(N,A).href}}import{devNull as Le}from"os";class ee extends Error{constructor(){super("Regular expression exceeded its execution budget");this.name="RegexTimeoutError"}}var Ie=`
2
+ var oe=import.meta.require;import{Buffer as y}from"buffer";import{config as M}from"@stacksjs/config";import{decrypt as K,encrypt as Ge}from"ts-security-crypto";function O(r){return r.startsWith("base64:")?r.slice(7):r}var H=1,L=2,se="stacks:encrypt:v2",I=600000,k=32,m=16,f=12,ie=64,p=new Map;function ae(r,e,t){return`${t}:${y.from(e).toString("base64")}:${r}`}async function F(r,e,t){let n=ae(r,e,t),o=p.get(n);if(o)return o.hits++,p.delete(n),p.set(n,o),o.key;let s=await crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"PBKDF2"},!1,["deriveKey"]),i=await crypto.subtle.deriveKey({name:"PBKDF2",salt:new Uint8Array(e),iterations:t,hash:"SHA-256"},s,{name:"AES-GCM",length:k*8},!1,["encrypt","decrypt"]);if(p.set(n,{key:i,hits:1}),p.size>ie){let a=p.keys().next().value;if(a!==void 0)p.delete(a)}return i}function _(r){if(!r.startsWith("base64:"))return null;let e=y.from(r.slice(7),"base64");return e.length>=k?new Uint8Array(e):null}async function V(r,e){let t=await crypto.subtle.importKey("raw",new Uint8Array(r),"HKDF",!1,["deriveKey"]);return await crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(e),info:new TextEncoder().encode(se)},t,{name:"AES-GCM",length:k*8},!1,["encrypt","decrypt"])}async function ce(r,e){let t=crypto.getRandomValues(new Uint8Array(m)),n=crypto.getRandomValues(new Uint8Array(f)),o=await V(e,t),s=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:n},o,new TextEncoder().encode(r))),i=new Uint8Array(1+m+f+s.byteLength);return i[0]=L,i.set(t,1),i.set(n,1+m),i.set(s,1+m+f),y.from(i).toString("base64")}async function ue(r,e){let t=y.from(r,"base64");if(t.length<1+m+f+16)return null;if(t[0]!==L)return null;let n=new Uint8Array(t.subarray(1,1+m)),o=new Uint8Array(t.subarray(1+m,1+m+f)),s=new Uint8Array(t.subarray(1+m+f)),i=await V(e,n);try{return new TextDecoder().decode(await crypto.subtle.decrypt({name:"AES-GCM",iv:o},i,s))}catch{return null}}async function me(r,e){let t=crypto.getRandomValues(new Uint8Array(m)),n=crypto.getRandomValues(new Uint8Array(f)),o=await F(e,t,I),s=await crypto.subtle.encrypt({name:"AES-GCM",iv:n},o,new TextEncoder().encode(r)),i=new Uint8Array(s),a=new Uint8Array(1+m+f+i.byteLength);return a[0]=H,a.set(t,1),a.set(n,1+m),a.set(i,1+m+f),y.from(a).toString("base64")}async function le(r,e){let t=y.from(r,"base64");if(t.length<1+m+f+16)return null;if(t[0]!==H)return null;let n=new Uint8Array(t.subarray(1,1+m)),o=new Uint8Array(t.subarray(1+m,1+m+f)),s=new Uint8Array(t.subarray(1+m+f)),i=await F(e,n,I);try{let a=await crypto.subtle.decrypt({name:"AES-GCM",iv:o},i,s);return new TextDecoder().decode(a)}catch{return null}}async function De(r,e){if(!r&&r!=="")throw Error("encrypt() requires a string message");let t=e||M.app.key;if(!t)throw Error("APP_KEY is not defined");let n=_(t);if(n)return await ce(r,n);return await me(r,O(t))}async function qe(r,e){if(!r)throw Error("decrypt() requires a non-empty encrypted string");let t=e||M.app.key;if(!t)throw Error("APP_KEY is not defined");let n=O(t),o=_(t);if(o){let i=await ue(r,o);if(i!==null)return i}let s=await le(r,n);if(s!==null)return s;try{return await K(r,n)}catch(i){if(n!==t)try{return await K(r,t)}catch{}throw i}}import{timingSafeEqual as G}from"crypto";import{base64Encode as Je,hashPassword as q,verifyPassword as D}from"ts-security-crypto";var w=null;function A(){if(w)return w;try{let{hashing:r}=oe("@stacksjs/config");return w=r||{},w}catch{return console.warn("[Security] Failed to load hashing config, using defaults"),w={driver:"bcrypt",bcrypt:{rounds:12},argon2:{memory:65536,time:2}},w}}function W(r){if(!r||typeof r!=="string")return"unknown";if(r.startsWith("$2a$")||r.startsWith("$2b$")||r.startsWith("$2y$"))return"bcrypt";if(r.startsWith("$argon2id$"))return"argon2id";if(r.startsWith("$argon2i$"))return"argon2i";if(r.startsWith("$argon2d$"))return"argon2d";return"unknown"}function fe(r){let e=W(r),t={};if(e==="bcrypt"){let n=r.match(/^\$2[aby]\$(\d{2})\$/);if(n&&n[1]!==void 0)t.rounds=Number.parseInt(n[1],10)}else if(e==="argon2id"||e==="argon2i"||e==="argon2d"){let n=r.match(/v=(\d+)/),o=r.match(/m=(\d+)/),s=r.match(/t=(\d+)/),i=r.match(/p=(\d+)/);if(n&&n[1]!==void 0)t.version=Number.parseInt(n[1],10);if(o&&o[1]!==void 0)t.memory=Number.parseInt(o[1],10);if(s&&s[1]!==void 0)t.rounds=Number.parseInt(s[1],10);if(i&&i[1]!==void 0)t.parallelism=Number.parseInt(i[1],10)}return{algorithm:e,options:t}}function Xe(r,e){let t=fe(r),n=A(),o=e?.algorithm||n.driver||"bcrypt",s=(c)=>{if(c==="argon2")return"argon2id";return c},i=s(t.algorithm),a=s(o);if(i!==a)return!0;if(i==="bcrypt"){let c=e?.rounds||n.bcrypt?.rounds||12;if(t.options.rounds!==c)return!0}if(i.startsWith("argon2")){let c=e?.memory||n.argon2?.memory||65536,u=e?.time||n.argon2?.time||2;if(t.options.memory!==c||t.options.rounds!==u)return!0}return!1}async function je(r,e){let t=A(),n=e?.algorithm||t.driver||"bcrypt";if(n==="bcrypt")return await ge(r,e?.rounds);if(n==="argon2"||n==="argon2id"||n==="argon2i"||n==="argon2d")return await he(r,{type:n==="argon2"?"argon2id":n,memory:e?.memory,time:e?.time});throw Error(`Unsupported hashing algorithm: ${n}`)}function Qe(r,e){let t=A(),n=e?.algorithm||t.driver||"bcrypt";if(n==="bcrypt")return Bun.password.hashSync(r,{algorithm:"bcrypt",cost:e?.rounds||t.bcrypt?.rounds||12});if(n==="argon2"||n==="argon2id"||n==="argon2i"||n==="argon2d")return Bun.password.hashSync(r,{algorithm:n==="argon2"?"argon2id":n,memoryCost:e?.memory||t.argon2?.memory||65536,timeCost:e?.time||t.argon2?.time||2});throw Error(`Unsupported hashing algorithm: ${n}`)}async function Ze(r,e){if(!r||!e)return!1;if(W(e)==="unknown")try{return await D(r,e)}catch{return!1}return await D(r,e)}async function ge(r,e){let t=A(),n=e||t.bcrypt?.rounds||12;return await q(r,{algorithm:"bcrypt",cost:n})}async function he(r,e){let t=A(),n=e?.type||"argon2id",o=e?.memory||t.argon2?.memory||65536,s=e?.time||t.argon2?.time||2;return await q(r,{algorithm:n,memoryCost:o,timeCost:s})}function et(r,e){let t=Buffer.from(r),n=Buffer.from(e);if(t.length!==n.length)return G(t,t),!1;return G(t,n)}import{generateKey as de}from"ts-security-crypto";function nt(){return de(32)}import{createHmac as pe,randomBytes as ye,timingSafeEqual as z}from"crypto";class l extends Error{reason;constructor(r,e="mismatch"){super(r);this.name="InvalidWebhookSignature",this.reason=e}}function B(r){if(!/^[0-9a-f]*$/i.test(r)||r.length%2!==0)throw new l("Signature is not valid hex","malformed");return Buffer.from(r,"hex")}function C(r,e){if(r.length!==e.length)return z(r,r),!1;return z(r,e)}function R(r,e,t){return pe(t,r).update(e).digest()}function we(r,e){let t=r.split(","),n=Number.NaN,o=[];for(let s of t){let i=s.indexOf("=");if(i===-1)continue;let a=s.slice(0,i).trim(),c=s.slice(i+1).trim();if(a==="t")n=Number.parseInt(c,10);else if(a==="v1")o.push(c)}if(!Number.isFinite(n)||o.length===0)throw new l(`${e} header is malformed`,"malformed");return{timestamp:n,signatures:o}}function Y(r,e,t,n,o){if(!r)throw new l(`${o.name} secret is empty`,"missing");if(!e)throw new l(`${o.header} header is missing`,"missing");let{timestamp:s,signatures:i}=we(e,o.header),a=n.toleranceSeconds??300,c=Math.floor((n.now??Date.now())/1000);if(Math.abs(c-s)>a)throw new l(`${o.name} timestamp is outside tolerance window`,"expired");let u=typeof t==="string"?t:t.toString("utf8"),g=R(r,`${s}.${u}`,"sha256");for(let b of i){let x;try{x=B(b)}catch{continue}if(C(g,x))return!0}throw new l(`${o.name} signature did not match`,"mismatch")}function be(r,e,t,n={}){return Y(r,e,t,n,{name:"Stripe webhook",header:"Stripe-Signature"})}function xe(r,e,t,n={}){return Y(r,e,t,n,{name:"Webhook",header:"Signature"})}function it(r,e,t={}){let n=(Array.isArray(r)?r:[r]).filter((a)=>typeof a==="string"&&a.length>0);if(!n.length)throw new l("Webhook signing secret is empty","missing");let o=t.timestamp??Math.floor(Date.now()/1000);if(!Number.isInteger(o)||o<0)throw new l("Webhook timestamp must be whole unix seconds","malformed");let s=typeof e==="string"?e:e.toString("utf8"),i=n.map((a)=>R(a,`${o}.${s}`,"sha256").toString("hex"));return{timestamp:o,signatures:i,header:[`t=${o}`,...i.map((a)=>`v1=${a}`)].join(",")}}function at(r="whsec_"){return`${r}${ye(32).toString("base64url")}`}function ve(r,e,t){if(!r)throw new l("GitHub webhook secret is empty","missing");if(!e)throw new l("X-Hub-Signature-256 header is missing","missing");let n="sha256=";if(!e.startsWith(n))throw new l("X-Hub-Signature-256 has wrong algorithm prefix","malformed");let o=B(e.slice(n.length)),s=R(r,t,"sha256");if(!C(s,o))throw new l("GitHub webhook signature did not match","mismatch");return!0}function Ae(r,e,t,n="sha256"){if(!r)throw new l("HMAC secret is empty","missing");if(!t)throw new l("Signature header is missing","missing");let o=t.indexOf("="),s=o!==-1&&/^[a-z0-9]+$/i.test(t.slice(0,o))?t.slice(o+1):t,i=B(s),a=R(r,e,n);if(!C(a,i))throw new l("HMAC signature did not match","mismatch");return!0}function ct(r,e,t,n,o={}){switch(r){case"stripe":return be(e,t,n,o);case"github":return ve(e,t,n);case"generic":return Ae(e,n,t,o.algorithm??"sha256");case"signed":return xe(e,t,n,o);default:throw new l(`Unknown webhook provider: ${String(r)}`,"malformed")}}import{createHash as Se,randomBytes as Re}from"crypto";function $e(r){return Se("sha256").update(r,"utf8").digest("hex")}function Ee(r,e=""){let t=e&&r.startsWith(e)?r.slice(e.length):r;if(t.length<=8)return`${e}\u2026`;return`${e}${t.slice(0,4)}\u2026${t.slice(-4)}`}function lt(r="",e=32){if(!Number.isInteger(e)||e<16)throw Error("An API key needs at least 16 random bytes");let t=`${r}${Re(e).toString("base64url")}`;return{key:t,hash:$e(t),hint:Ee(t,r)}}import{lookup as Ue}from"dns/promises";import{isIP as U}from"net";class d extends Error{reason;constructor(r,e){super(r);this.name="OutboundUrlError",this.reason=e}}function X(r){let e=r.split(".");if(e.length!==4)return null;let t=e.map((n)=>/^\d{1,3}$/.test(n)?Number(n):Number.NaN);return t.every((n)=>Number.isInteger(n)&&n>=0&&n<=255)?t:null}function E(r){let e=X(r);if(!e)return!0;let[t,n,o]=e;return t===0||t===10||t===100&&n>=64&&n<=127||t===127||t===169&&n===254||t===172&&n>=16&&n<=31||t===192&&n===0&&(o===0||o===2)||t===192&&n===88&&o===99||t===192&&n===168||t===198&&(n===18||n===19)||t===198&&n===51&&o===100||t===203&&n===0&&o===113||t>=224}function Pe(r){let e=r.toLowerCase(),t=e.indexOf("%");if(t!==-1)e=e.slice(0,t);let n=e.match(/^(.*:)(\d+\.\d+\.\d+\.\d+)$/);if(n){let u=X(n[2]);if(!u)return null;e=`${n[1]}${(u[0]<<8|u[1]).toString(16)}:${(u[2]<<8|u[3]).toString(16)}`}let o=e.split("::");if(o.length>2)return null;let s=o[0]?o[0].split(":"):[],i=o.length===2&&o[1]?o[1].split(":"):[],a=8-s.length-i.length;if(o.length===1&&a!==0||a<0)return null;let c=[...s,...Array.from({length:o.length===2?a:0},()=>"0"),...i];if(c.length!==8||c.some((u)=>!/^[0-9a-f]{1,4}$/.test(u)))return null;return c.map((u)=>Number.parseInt(u,16))}function Te(r){let e=Pe(r);if(!e)return!0;let t=`${e[6]>>8}.${e[6]&255}.${e[7]>>8}.${e[7]&255}`;if(e.slice(0,7).every((n)=>n===0))return!0;if(e.slice(0,5).every((n)=>n===0)&&e[5]===65535)return E(t);if(e[0]===100&&e[1]===65435&&e.slice(2,6).every((n)=>n===0))return E(t);if(e[0]===8194)return E(`${e[1]>>8}.${e[1]&255}.${e[2]>>8}.${e[2]&255}`);return(e[0]&65024)===64512||(e[0]&65472)===65152||(e[0]&65280)===65280||e[0]===8193&&e[1]===3512||e[0]===8193&&e[1]<512||e[0]===256}function j(r){let e=r.replace(/^\[|\]$/g,""),t=U(e.split("%")[0]);if(t===4)return E(e);if(t===6)return Te(e);return!0}var ke=[".localhost",".local",".internal",".localdomain",".home.arpa"];function Be(r,e={}){let t;try{t=new URL(r)}catch{return{reason:"invalid",message:"Not a valid URL"}}if(!(e.allowHttp?["https:","http:"]:["https:"]).includes(t.protocol))return{reason:"scheme",message:e.allowHttp?"The URL must start with https:// or http://":"The URL must start with https://"};if(t.username||t.password)return{reason:"credentials",message:"The URL must not contain a username or password"};if(e.allowPrivate)return null;let o=t.hostname.replace(/^\[|\]$/g,"").toLowerCase().replace(/\.$/,"");if(!o)return{reason:"invalid",message:"Not a valid URL"};if(U(o.split("%")[0]))return j(o)?{reason:"private",message:"The URL points at a private or local address"}:null;if(o==="localhost"||!o.includes(".")||ke.some((s)=>o.endsWith(s)))return{reason:"private",message:"The URL points at a private or local address"};return null}async function Ce(r){return(await Ue(r,{all:!0,verbatim:!0})).map((t)=>t.address)}async function dt(r,e={}){return(await Q(r,e)).parsed}async function Q(r,e){let t=Be(r,e);if(t)throw new d(t.message,t.reason);let n=new URL(r),o=n.hostname.replace(/^\[|\]$/g,"");if(e.allowPrivate||U(o.split("%")[0]))return{parsed:n,addresses:[]};let s;try{s=await(e.resolve??Ce)(o)}catch{throw new d(`Could not resolve ${o}`,"unresolvable")}if(!s.length)throw new d(`Could not resolve ${o}`,"unresolvable");if(s.some(j))throw new d("The URL points at a private or local address","private");return{parsed:n,addresses:s}}var Ne=new Set([301,302,303,307,308]),Ke=["authorization","cookie","proxy-authorization"];function Me(r,e,t){if(!e)return[r.href,t];let n=new URL(r.href);n.hostname=U(e)===6?`[${e}]`:e;let o=new Headers(t.headers);o.set("Host",r.host);let s={...t,headers:o};if(r.protocol==="https:")s.tls={serverName:r.hostname.replace(/^\[|\]$/g,"")};return[n.href,s]}async function Z(r){await r.body?.cancel().catch(()=>{})}async function Oe(r,e){e.throwIfAborted();let t=()=>{},n=new Promise((o,s)=>{t=()=>s(e.reason),e.addEventListener("abort",t,{once:!0})});try{return await Promise.race([Promise.resolve().then(r),n])}finally{e.removeEventListener("abort",t)}}async function J(r,e){let t=Number(r.headers.get("content-length")??Number.NaN);if(Number.isFinite(t)&&t>e)throw await Z(r),new d(`The response is ${t} bytes, over the ${e} byte limit`,"too-large");if(!r.body)return r;let n=[],o=0,s=r.body.getReader();try{for(;;){let{done:c,value:u}=await s.read();if(c)break;if(o+=u.byteLength,o>e)throw await s.cancel(),new d(`The response is over the ${e} byte limit`,"too-large");n.push(u)}}finally{s.releaseLock()}let i=new Uint8Array(o),a=0;for(let c of n)i.set(c,a),a+=c.byteLength;return new Response(i,{status:r.status,statusText:r.statusText,headers:r.headers})}async function pt(r,e={},t={}){let n=t.maxRedirects??3,o=t.maxBytes??5242880,s=t.timeoutMs??1e4,i=AbortSignal.timeout(s),a=e.signal?AbortSignal.any([e.signal,i]):i,c=e.body==null||typeof e.body==="string"||e.body instanceof Uint8Array||e.body instanceof ArrayBuffer,u=r,g={...e,signal:a,redirect:"manual"},b="";for(let x=0;;x++){a.throwIfAborted();let{parsed:S,addresses:te}=await Oe(()=>Q(u,t),a);if(b&&S.origin!==b){let v=new Headers(g.headers);for(let T of Ke)v.delete(T);g={...g,headers:v}}b=S.origin;let[re,ne]=Me(S,te[0],g),h=await fetch(re,ne);if(!Ne.has(h.status))return await J(h,o);let N=h.headers.get("location");if(!N)return await J(h,o);if(await Z(h),x>=n)throw new d(`The URL redirected more than ${n} times`,"redirects");if(h.status!==303&&g.body!=null&&!c)throw new d("The URL redirected, and a streaming request body cannot be sent again","unreplayable-body");let P=(g.method??"GET").toUpperCase();if((h.status===301||h.status===302)&&P==="POST"||h.status===303&&P!=="GET"&&P!=="HEAD"){let v=new Headers(g.headers);for(let T of["content-encoding","content-language","content-location","content-type","content-length"])v.delete(T);g={...g,headers:v,method:"GET",body:void 0}}u=new URL(N,S).href}}import{devNull as He}from"os";class ee extends Error{constructor(){super("Regular expression exceeded its execution budget");this.name="RegexTimeoutError"}}var Le=`
3
3
  const { source, flags, input } = JSON.parse(await Bun.stdin.text());
4
4
  try {
5
5
  const match = new RegExp(source, flags).exec(input);
@@ -7,4 +7,4 @@ try {
7
7
  } catch {
8
8
  console.log(JSON.stringify({ invalid: true }));
9
9
  }
10
- `;function wt(t,e,r={}){let n=r.timeoutMs??25;if(!Number.isInteger(n)||n<1||n>1000)throw RangeError("Regex timeout must be an integer between 1 and 1000 milliseconds");if(typeof t!=="string"||t.length>8192||typeof e!=="string"||e.length>65536)throw RangeError("Regex source or input exceeds its size limit");if(r.flags!==void 0&&typeof r.flags!=="string")throw TypeError("Regex flags must be a string");if(r.flags!==void 0&&r.flags.length>8)throw SyntaxError("Invalid regular expression flags");let s=Bun.which("bun");if(!s)throw Error("Bounded regex matching requires the Bun executable on PATH");let o=Bun.spawnSync([s,`--config=${Le}`,"--no-env-file","--no-install","--eval",Ie],{env:{},stdin:Buffer.from(JSON.stringify({source:t,flags:r.flags??"",input:e})),stdout:"pipe",stderr:"ignore",timeout:n,killSignal:"SIGKILL",maxBuffer:524288});if(o.signalCode==="SIGKILL")throw new ee;if(o.exitCode!==0)throw Error("Isolated regex execution failed");let i=JSON.parse(o.stdout.toString());if(i.invalid)throw SyntaxError("Invalid regular expression");if(i.match===null)return null;if(!i.match||typeof i.match.value!=="string"||!Number.isInteger(i.match.index))throw Error("Invalid isolated regex response");return i.match}export{l as InvalidWebhookSignature,d as OutboundUrlError,ee as RegexTimeoutError,Ee as apiKeyHint,he as argon2Encode,ht as assertPublicOutboundUrl,Je as base64Encode,ge as bcryptEncode,Qe as check,qe as decrypt,W as detectAlgorithm,De as encrypt,dt as fetchPublicUrl,mt as generateApiKey,rt as generateAppKey,it as generateWebhookSecret,Se as hashApiKey,Qe as hashCheck,W as hashDetectAlgorithm,fe as hashInfo,je as hashMake,Xe as hashNeedsRehash,fe as info,j as isPrivateAddress,Ge as legacyEncrypt,je as make,je as makeHash,wt as matchBoundedRegex,Xe as needsRehash,ke as outboundUrlProblem,ot as signWebhook,Ze as timingSafeEqualString,ve as verifyGithub,Qe as verifyHash,Ae as verifyHmac,xe as verifySignedWebhook,be as verifyStripe,at as verifyWebhook};
10
+ `;function bt(r,e,t={}){let n=t.timeoutMs??25;if(!Number.isInteger(n)||n<1||n>1000)throw RangeError("Regex timeout must be an integer between 1 and 1000 milliseconds");if(typeof r!=="string"||r.length>8192||typeof e!=="string"||e.length>65536)throw RangeError("Regex source or input exceeds its size limit");if(t.flags!==void 0&&typeof t.flags!=="string")throw TypeError("Regex flags must be a string");if(t.flags!==void 0&&t.flags.length>8)throw SyntaxError("Invalid regular expression flags");let o=Bun.which("bun");if(!o)throw Error("Bounded regex matching requires the Bun executable on PATH");let s=Bun.spawnSync([o,`--config=${He}`,"--no-env-file","--no-install","--eval",Le],{env:{},stdin:Buffer.from(JSON.stringify({source:r,flags:t.flags??"",input:e})),stdout:"pipe",stderr:"ignore",timeout:n,killSignal:"SIGKILL",maxBuffer:524288});if(s.signalCode==="SIGKILL")throw new ee;if(s.exitCode!==0)throw Error("Isolated regex execution failed");let i=JSON.parse(s.stdout.toString());if(i.invalid)throw SyntaxError("Invalid regular expression");if(i.match===null)return null;if(!i.match||typeof i.match.value!=="string"||!Number.isInteger(i.match.index))throw Error("Invalid isolated regex response");return i.match}export{l as InvalidWebhookSignature,d as OutboundUrlError,ee as RegexTimeoutError,Ee as apiKeyHint,he as argon2Encode,dt as assertPublicOutboundUrl,Je as base64Encode,ge as bcryptEncode,Ze as check,qe as decrypt,W as detectAlgorithm,De as encrypt,pt as fetchPublicUrl,lt as generateApiKey,nt as generateAppKey,at as generateWebhookSecret,$e as hashApiKey,Ze as hashCheck,W as hashDetectAlgorithm,fe as hashInfo,je as hashMake,Qe as hashMakeSync,Xe as hashNeedsRehash,fe as info,j as isPrivateAddress,Ge as legacyEncrypt,je as make,je as makeHash,Qe as makeHashSync,Qe as makeSync,bt as matchBoundedRegex,Xe as needsRehash,Be as outboundUrlProblem,it as signWebhook,et as timingSafeEqualString,ve as verifyGithub,Ze as verifyHash,Ae as verifyHmac,xe as verifySignedWebhook,be as verifyStripe,ct as verifyWebhook};
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@stacksjs/security",
3
3
  "type": "module",
4
4
  "sideEffects": false,
5
- "version": "0.75.568",
5
+ "version": "0.75.571",
6
6
  "description": "The Stacks framework security.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -69,13 +69,13 @@
69
69
  "prepublishOnly": "bun run build"
70
70
  },
71
71
  "dependencies": {
72
- "@stacksjs/config": "0.75.568",
72
+ "@stacksjs/config": "0.75.571",
73
73
  "ts-security-crypto": "0.0.2"
74
74
  },
75
75
  "devDependencies": {
76
- "@stacksjs/env": "0.75.568",
77
- "@stacksjs/types": "0.75.568",
78
- "@stacksjs/validation": "0.75.568",
76
+ "@stacksjs/env": "0.75.571",
77
+ "@stacksjs/types": "0.75.571",
78
+ "@stacksjs/validation": "0.75.571",
79
79
  "better-dx": "^0.2.24"
80
80
  }
81
81
  }