@stacksjs/security 0.75.163 → 0.75.164

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.
@@ -0,0 +1,21 @@
1
+ /** SHA-256 hex of an API key, for storing and for looking one up. */
2
+ export declare function hashApiKey(key: string): string;
3
+ /** `prefix` + a few characters from each end of the secret, e.g. `hq_live_AbCd…wxYz`. */
4
+ export declare function apiKeyHint(key: string, prefix?: string): string;
5
+ /**
6
+ * A new API key: `prefix` (say `myapp_live_`) followed by `bytes` random bytes
7
+ * in base64url. The prefix makes a leaked key recognisable to secret scanners
8
+ * and to the person who finds it.
9
+ */
10
+ export declare function generateApiKey(prefix?: string, bytes?: number): GeneratedApiKey;
11
+ /**
12
+ * API keys a server hands out to integrations: a random secret shown to its
13
+ * owner once, stored only as a SHA-256 digest. The secret is 32 random bytes,
14
+ * so a fast hash is enough; there is nothing to brute-force the way there is
15
+ * with a password.
16
+ */
17
+ export declare interface GeneratedApiKey {
18
+ key: string
19
+ hash: string
20
+ hint: string
21
+ }
package/dist/index.d.ts CHANGED
@@ -2,3 +2,5 @@ export * from './crypt';
2
2
  export * from './hash';
3
3
  export * from './key';
4
4
  export * from './webhook';
5
+ export * from './api-key';
6
+ export * from './outbound';
package/dist/index.js CHANGED
@@ -1,2 +1,2 @@
1
1
  // @bun
2
- var V=import.meta.require;import{Buffer as l}from"buffer";import{config as E}from"@stacksjs/config";import{decrypt as A,encrypt as nr}from"ts-security-crypto";function k(r){return r.startsWith("base64:")?r.slice(7):r}var S=1,B=600000,U=32,f=16,g=12,D=64,u=new Map;function O(r,e,t){return`${t}:${l.from(e).toString("base64")}:${r}`}async function C(r,e,t){let n=O(r,e,t),i=u.get(n);if(i)return i.hits++,u.delete(n),u.set(n,i),i.key;let o=await crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"PBKDF2"},!1,["deriveKey"]),s=await crypto.subtle.deriveKey({name:"PBKDF2",salt:new Uint8Array(e),iterations:t,hash:"SHA-256"},o,{name:"AES-GCM",length:U*8},!1,["encrypt","decrypt"]);if(u.set(n,{key:s,hits:1}),u.size>D){let a=u.keys().next().value;if(a!==void 0)u.delete(a)}return s}async function W(r,e){let t=crypto.getRandomValues(new Uint8Array(f)),n=crypto.getRandomValues(new Uint8Array(g)),i=await C(e,t,B),o=await crypto.subtle.encrypt({name:"AES-GCM",iv:n},i,new TextEncoder().encode(r)),s=new Uint8Array(o),a=new Uint8Array(1+f+g+s.byteLength);return a[0]=S,a.set(t,1),a.set(n,1+f),a.set(s,1+f+g),l.from(a).toString("base64")}async function q(r,e){let t=l.from(r,"base64");if(t.length<1+f+g+16)return null;if(t[0]!==S)return null;let n=new Uint8Array(t.subarray(1,1+f)),i=new Uint8Array(t.subarray(1+f,1+f+g)),o=new Uint8Array(t.subarray(1+f+g)),s=await C(e,n,B);try{let a=await crypto.subtle.decrypt({name:"AES-GCM",iv:i},s,o);return new TextDecoder().decode(a)}catch{return null}}async function ir(r,e){if(!r&&r!=="")throw Error("encrypt() requires a string message");let t=e||E.app.key;if(!t)throw Error("APP_KEY is not defined");return await W(r,k(t))}async function or(r,e){if(!r)throw Error("decrypt() requires a non-empty encrypted string");let t=e||E.app.key;if(!t)throw Error("APP_KEY is not defined");let n=k(t),i=await q(r,n);if(i!==null)return i;try{return await A(r,n)}catch(o){if(n!==t)try{return await A(r,t)}catch{}throw o}}function sr(){u.clear()}import{timingSafeEqual as P}from"crypto";import{base64Encode as fr,hashPassword as M,verifyPassword as K}from"ts-security-crypto";var h=null;function p(){if(h)return h;try{let{hashing:r}=V("@stacksjs/config");return h=r||{},h}catch{return console.warn("[Security] Failed to load hashing config, using defaults"),h={driver:"bcrypt",bcrypt:{rounds:12},argon2:{memory:65536,time:2}},h}}function v(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 R(r){let e=v(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+)/),i=r.match(/m=(\d+)/),o=r.match(/t=(\d+)/),s=r.match(/p=(\d+)/);if(n&&n[1]!==void 0)t.version=Number.parseInt(n[1],10);if(i&&i[1]!==void 0)t.memory=Number.parseInt(i[1],10);if(o&&o[1]!==void 0)t.rounds=Number.parseInt(o[1],10);if(s&&s[1]!==void 0)t.parallelism=Number.parseInt(s[1],10)}return{algorithm:e,options:t}}function ur(r,e){let t=R(r),n=p(),i=e?.algorithm||n.driver||"bcrypt",o=(m)=>{if(m==="argon2")return"argon2id";return m},s=o(t.algorithm),a=o(i);if(s!==a)return!0;if(s==="bcrypt"){let m=e?.rounds||n.bcrypt?.rounds||12;if(t.options.rounds!==m)return!0}if(s.startsWith("argon2")){let m=e?.memory||n.argon2?.memory||65536,d=e?.time||n.argon2?.time||2;if(t.options.memory!==m||t.options.rounds!==d)return!0}return!1}async function gr(r,e){let t=p(),n=e?.algorithm||t.driver||"bcrypt";if(n==="bcrypt")return await Y(r,e?.rounds);if(n==="argon2"||n==="argon2id"||n==="argon2i"||n==="argon2d")return await F(r,{type:n==="argon2"?"argon2id":n,memory:e?.memory,time:e?.time});throw Error(`Unsupported hashing algorithm: ${n}`)}async function hr(r,e){if(!r||!e)return!1;if(v(e)==="unknown")try{return await K(r,e)}catch{return!1}return await K(r,e)}async function Y(r,e){let t=p(),n=e||t.bcrypt?.rounds||12;return await M(r,{algorithm:"bcrypt",cost:n})}async function F(r,e){let t=p(),n=e?.type||"argon2id",i=e?.memory||t.argon2?.memory||65536,o=e?.time||t.argon2?.time||2;return await M(r,{algorithm:n,memoryCost:i,timeCost:o})}function pr(r,e){let t=Buffer.from(r),n=Buffer.from(e);if(t.length!==n.length)return P(t,t),!1;return P(t,n)}import{generateKey as G}from"ts-security-crypto";function lr(){return G(32)}import{createHmac as z,randomBytes as X,timingSafeEqual as H}from"crypto";class c extends Error{reason;constructor(r,e="mismatch"){super(r);this.name="InvalidWebhookSignature",this.reason=e}}function w(r){if(!/^[0-9a-f]*$/i.test(r)||r.length%2!==0)throw new c("Signature is not valid hex","malformed");return Buffer.from(r,"hex")}function b(r,e){if(r.length!==e.length)return H(r,r),!1;return H(r,e)}function y(r,e,t){return z(t,r).update(e).digest()}function I(r,e){let t=r.split(","),n=Number.NaN,i=[];for(let o of t){let s=o.indexOf("=");if(s===-1)continue;let a=o.slice(0,s).trim(),m=o.slice(s+1).trim();if(a==="t")n=Number.parseInt(m,10);else if(a==="v1")i.push(m)}if(!Number.isFinite(n)||i.length===0)throw new c(`${e} header is malformed`,"malformed");return{timestamp:n,signatures:i}}function T(r,e,t,n,i){if(!r)throw new c(`${i.name} secret is empty`,"missing");if(!e)throw new c(`${i.header} header is missing`,"missing");let{timestamp:o,signatures:s}=I(e,i.header),a=n.toleranceSeconds??300,m=Math.floor((n.now??Date.now())/1000);if(Math.abs(m-o)>a)throw new c(`${i.name} timestamp is outside tolerance window`,"expired");let d=typeof t==="string"?t:t.toString("utf8"),_=y(r,`${o}.${d}`,"sha256");for(let N of s){let x;try{x=w(N)}catch{continue}if(b(_,x))return!0}throw new c(`${i.name} signature did not match`,"mismatch")}function L(r,e,t,n={}){return T(r,e,t,n,{name:"Stripe webhook",header:"Stripe-Signature"})}function j(r,e,t,n={}){return T(r,e,t,n,{name:"Webhook",header:"Signature"})}function xr(r,e,t={}){let n=(Array.isArray(r)?r:[r]).filter((a)=>typeof a==="string"&&a.length>0);if(!n.length)throw new c("Webhook signing secret is empty","missing");let i=t.timestamp??Math.floor(Date.now()/1000);if(!Number.isInteger(i)||i<0)throw new c("Webhook timestamp must be whole unix seconds","malformed");let o=typeof e==="string"?e:e.toString("utf8"),s=n.map((a)=>y(a,`${i}.${o}`,"sha256").toString("hex"));return{timestamp:i,signatures:s,header:[`t=${i}`,...s.map((a)=>`v1=${a}`)].join(",")}}function Ar(r="whsec_"){return`${r}${X(32).toString("base64url")}`}function J(r,e,t){if(!r)throw new c("GitHub webhook secret is empty","missing");if(!e)throw new c("X-Hub-Signature-256 header is missing","missing");let n="sha256=";if(!e.startsWith(n))throw new c("X-Hub-Signature-256 has wrong algorithm prefix","malformed");let i=w(e.slice(n.length)),o=y(r,t,"sha256");if(!b(o,i))throw new c("GitHub webhook signature did not match","mismatch");return!0}function Q(r,e,t,n="sha256"){if(!r)throw new c("HMAC secret is empty","missing");if(!t)throw new c("Signature header is missing","missing");let i=t.indexOf("="),o=i!==-1&&/^[a-z0-9]+$/i.test(t.slice(0,i))?t.slice(i+1):t,s=w(o),a=y(r,e,n);if(!b(a,s))throw new c("HMAC signature did not match","mismatch");return!0}function $r(r,e,t,n,i={}){switch(r){case"stripe":return L(e,t,n,i);case"github":return J(e,t,n);case"generic":return Q(e,n,t,i.algorithm??"sha256");case"signed":return j(e,t,n,i);default:throw new c(`Unknown webhook provider: ${String(r)}`,"malformed")}}export{c as InvalidWebhookSignature,sr as _resetCryptCacheForTests,F as argon2Encode,fr as base64Encode,Y as bcryptEncode,hr as check,or as decrypt,v as detectAlgorithm,ir as encrypt,lr as generateAppKey,Ar as generateWebhookSecret,hr as hashCheck,v as hashDetectAlgorithm,R as hashInfo,gr as hashMake,ur as hashNeedsRehash,R as info,nr as legacyEncrypt,gr as make,gr as makeHash,ur as needsRehash,xr as signWebhook,pr as timingSafeEqualString,J as verifyGithub,hr as verifyHash,Q as verifyHmac,j as verifySignedWebhook,L as verifyStripe,$r as verifyWebhook};
2
+ var V=import.meta.require;import{Buffer as b}from"buffer";import{config as S}from"@stacksjs/config";import{decrypt as P,encrypt as wt}from"ts-security-crypto";function k(r){return r.startsWith("base64:")?r.slice(7):r}var U=1,B=600000,W=32,f=16,l=12,D=64,g=new Map;function I(r,t,e){return`${e}:${b.from(t).toString("base64")}:${r}`}async function C(r,t,e){let n=I(r,t,e),s=g.get(n);if(s)return s.hits++,g.delete(n),g.set(n,s),s.key;let o=await crypto.subtle.importKey("raw",new TextEncoder().encode(r),{name:"PBKDF2"},!1,["deriveKey"]),a=await crypto.subtle.deriveKey({name:"PBKDF2",salt:new Uint8Array(t),iterations:e,hash:"SHA-256"},o,{name:"AES-GCM",length:W*8},!1,["encrypt","decrypt"]);if(g.set(n,{key:a,hits:1}),g.size>D){let i=g.keys().next().value;if(i!==void 0)g.delete(i)}return a}async function G(r,t){let e=crypto.getRandomValues(new Uint8Array(f)),n=crypto.getRandomValues(new Uint8Array(l)),s=await C(t,e,B),o=await crypto.subtle.encrypt({name:"AES-GCM",iv:n},s,new TextEncoder().encode(r)),a=new Uint8Array(o),i=new Uint8Array(1+f+l+a.byteLength);return i[0]=U,i.set(e,1),i.set(n,1+f),i.set(a,1+f+l),b.from(i).toString("base64")}async function q(r,t){let e=b.from(r,"base64");if(e.length<1+f+l+16)return null;if(e[0]!==U)return null;let n=new Uint8Array(e.subarray(1,1+f)),s=new Uint8Array(e.subarray(1+f,1+f+l)),o=new Uint8Array(e.subarray(1+f+l)),a=await C(t,n,B);try{let i=await crypto.subtle.decrypt({name:"AES-GCM",iv:s},a,o);return new TextDecoder().decode(i)}catch{return null}}async function bt(r,t){if(!r&&r!=="")throw Error("encrypt() requires a string message");let e=t||S.app.key;if(!e)throw Error("APP_KEY is not defined");return await G(r,k(e))}async function xt(r,t){if(!r)throw Error("decrypt() requires a non-empty encrypted string");let e=t||S.app.key;if(!e)throw Error("APP_KEY is not defined");let n=k(e),s=await q(r,n);if(s!==null)return s;try{return await P(r,n)}catch(o){if(n!==e)try{return await P(r,e)}catch{}throw o}}function $t(){g.clear()}import{timingSafeEqual as K}from"crypto";import{base64Encode as Pt,hashPassword as F,verifyPassword as R}from"ts-security-crypto";var h=null;function d(){if(h)return h;try{let{hashing:r}=V("@stacksjs/config");return h=r||{},h}catch{return console.warn("[Security] Failed to load hashing config, using defaults"),h={driver:"bcrypt",bcrypt:{rounds:12},argon2:{memory:65536,time:2}},h}}function N(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 Y(r){let t=N(r),e={};if(t==="bcrypt"){let n=r.match(/^\$2[aby]\$(\d{2})\$/);if(n&&n[1]!==void 0)e.rounds=Number.parseInt(n[1],10)}else if(t==="argon2id"||t==="argon2i"||t==="argon2d"){let n=r.match(/v=(\d+)/),s=r.match(/m=(\d+)/),o=r.match(/t=(\d+)/),a=r.match(/p=(\d+)/);if(n&&n[1]!==void 0)e.version=Number.parseInt(n[1],10);if(s&&s[1]!==void 0)e.memory=Number.parseInt(s[1],10);if(o&&o[1]!==void 0)e.rounds=Number.parseInt(o[1],10);if(a&&a[1]!==void 0)e.parallelism=Number.parseInt(a[1],10)}return{algorithm:t,options:e}}function St(r,t){let e=Y(r),n=d(),s=t?.algorithm||n.driver||"bcrypt",o=(u)=>{if(u==="argon2")return"argon2id";return u},a=o(e.algorithm),i=o(s);if(a!==i)return!0;if(a==="bcrypt"){let u=t?.rounds||n.bcrypt?.rounds||12;if(e.options.rounds!==u)return!0}if(a.startsWith("argon2")){let u=t?.memory||n.argon2?.memory||65536,m=t?.time||n.argon2?.time||2;if(e.options.memory!==u||e.options.rounds!==m)return!0}return!1}async function kt(r,t){let e=d(),n=t?.algorithm||e.driver||"bcrypt";if(n==="bcrypt")return await z(r,t?.rounds);if(n==="argon2"||n==="argon2id"||n==="argon2i"||n==="argon2d")return await X(r,{type:n==="argon2"?"argon2id":n,memory:t?.memory,time:t?.time});throw Error(`Unsupported hashing algorithm: ${n}`)}async function Ut(r,t){if(!r||!t)return!1;if(N(t)==="unknown")try{return await R(r,t)}catch{return!1}return await R(r,t)}async function z(r,t){let e=d(),n=t||e.bcrypt?.rounds||12;return await F(r,{algorithm:"bcrypt",cost:n})}async function X(r,t){let e=d(),n=t?.type||"argon2id",s=t?.memory||e.argon2?.memory||65536,o=t?.time||e.argon2?.time||2;return await F(r,{algorithm:n,memoryCost:s,timeCost:o})}function Bt(r,t){let e=Buffer.from(r),n=Buffer.from(t);if(e.length!==n.length)return K(e,e),!1;return K(e,n)}import{generateKey as j}from"ts-security-crypto";function Rt(){return j(32)}import{createHmac as J,randomBytes as Q,timingSafeEqual as O}from"crypto";class c extends Error{reason;constructor(r,t="mismatch"){super(r);this.name="InvalidWebhookSignature",this.reason=t}}function x(r){if(!/^[0-9a-f]*$/i.test(r)||r.length%2!==0)throw new c("Signature is not valid hex","malformed");return Buffer.from(r,"hex")}function v(r,t){if(r.length!==t.length)return O(r,r),!1;return O(r,t)}function y(r,t,e){return J(e,r).update(t).digest()}function Z(r,t){let e=r.split(","),n=Number.NaN,s=[];for(let o of e){let a=o.indexOf("=");if(a===-1)continue;let i=o.slice(0,a).trim(),u=o.slice(a+1).trim();if(i==="t")n=Number.parseInt(u,10);else if(i==="v1")s.push(u)}if(!Number.isFinite(n)||s.length===0)throw new c(`${t} header is malformed`,"malformed");return{timestamp:n,signatures:s}}function T(r,t,e,n,s){if(!r)throw new c(`${s.name} secret is empty`,"missing");if(!t)throw new c(`${s.header} header is missing`,"missing");let{timestamp:o,signatures:a}=Z(t,s.header),i=n.toleranceSeconds??300,u=Math.floor((n.now??Date.now())/1000);if(Math.abs(u-o)>i)throw new c(`${s.name} timestamp is outside tolerance window`,"expired");let m=typeof e==="string"?e:e.toString("utf8"),L=y(r,`${o}.${m}`,"sha256");for(let _ of a){let E;try{E=x(_)}catch{continue}if(v(L,E))return!0}throw new c(`${s.name} signature did not match`,"mismatch")}function tt(r,t,e,n={}){return T(r,t,e,n,{name:"Stripe webhook",header:"Stripe-Signature"})}function rt(r,t,e,n={}){return T(r,t,e,n,{name:"Webhook",header:"Signature"})}function Ot(r,t,e={}){let n=(Array.isArray(r)?r:[r]).filter((i)=>typeof i==="string"&&i.length>0);if(!n.length)throw new c("Webhook signing secret is empty","missing");let s=e.timestamp??Math.floor(Date.now()/1000);if(!Number.isInteger(s)||s<0)throw new c("Webhook timestamp must be whole unix seconds","malformed");let o=typeof t==="string"?t:t.toString("utf8"),a=n.map((i)=>y(i,`${s}.${o}`,"sha256").toString("hex"));return{timestamp:s,signatures:a,header:[`t=${s}`,...a.map((i)=>`v1=${i}`)].join(",")}}function Tt(r="whsec_"){return`${r}${Q(32).toString("base64url")}`}function et(r,t,e){if(!r)throw new c("GitHub webhook secret is empty","missing");if(!t)throw new c("X-Hub-Signature-256 header is missing","missing");let n="sha256=";if(!t.startsWith(n))throw new c("X-Hub-Signature-256 has wrong algorithm prefix","malformed");let s=x(t.slice(n.length)),o=y(r,e,"sha256");if(!v(o,s))throw new c("GitHub webhook signature did not match","mismatch");return!0}function nt(r,t,e,n="sha256"){if(!r)throw new c("HMAC secret is empty","missing");if(!e)throw new c("Signature header is missing","missing");let s=e.indexOf("="),o=s!==-1&&/^[a-z0-9]+$/i.test(e.slice(0,s))?e.slice(s+1):e,a=x(o),i=y(r,t,n);if(!v(i,a))throw new c("HMAC signature did not match","mismatch");return!0}function Ht(r,t,e,n,s={}){switch(r){case"stripe":return tt(t,e,n,s);case"github":return et(t,e,n);case"generic":return nt(t,n,e,s.algorithm??"sha256");case"signed":return rt(t,e,n,s);default:throw new c(`Unknown webhook provider: ${String(r)}`,"malformed")}}import{createHash as st,randomBytes as ot}from"crypto";function it(r){return st("sha256").update(r,"utf8").digest("hex")}function at(r,t=""){let e=t&&r.startsWith(t)?r.slice(t.length):r;if(e.length<=8)return`${t}\u2026`;return`${t}${e.slice(0,4)}\u2026${e.slice(-4)}`}function _t(r="",t=32){if(!Number.isInteger(t)||t<16)throw Error("An API key needs at least 16 random bytes");let e=`${r}${ot(t).toString("base64url")}`;return{key:e,hash:it(e),hint:at(e,r)}}import{lookup as ct}from"dns/promises";import{isIP as A}from"net";class p extends Error{reason;constructor(r,t){super(r);this.name="OutboundUrlError",this.reason=t}}function H(r){let t=r.split(".");if(t.length!==4)return null;let e=t.map((n)=>/^\d{1,3}$/.test(n)?Number(n):Number.NaN);return e.every((n)=>Number.isInteger(n)&&n>=0&&n<=255)?e:null}function w(r){let t=H(r);if(!t)return!0;let[e,n,s]=t;return e===0||e===10||e===100&&n>=64&&n<=127||e===127||e===169&&n===254||e===172&&n>=16&&n<=31||e===192&&n===0&&(s===0||s===2)||e===192&&n===88&&s===99||e===192&&n===168||e===198&&(n===18||n===19)||e===198&&n===51&&s===100||e===203&&n===0&&s===113||e>=224}function ut(r){let t=r.toLowerCase(),e=t.indexOf("%");if(e!==-1)t=t.slice(0,e);let n=t.match(/^(.*:)(\d+\.\d+\.\d+\.\d+)$/);if(n){let m=H(n[2]);if(!m)return null;t=`${n[1]}${(m[0]<<8|m[1]).toString(16)}:${(m[2]<<8|m[3]).toString(16)}`}let s=t.split("::");if(s.length>2)return null;let o=s[0]?s[0].split(":"):[],a=s.length===2&&s[1]?s[1].split(":"):[],i=8-o.length-a.length;if(s.length===1&&i!==0||i<0)return null;let u=[...o,...Array.from({length:s.length===2?i:0},()=>"0"),...a];if(u.length!==8||u.some((m)=>!/^[0-9a-f]{1,4}$/.test(m)))return null;return u.map((m)=>Number.parseInt(m,16))}function mt(r){let t=ut(r);if(!t)return!0;let e=`${t[6]>>8}.${t[6]&255}.${t[7]>>8}.${t[7]&255}`;if(t.slice(0,7).every((n)=>n===0))return!0;if(t.slice(0,5).every((n)=>n===0)&&t[5]===65535)return w(e);if(t[0]===100&&t[1]===65435&&t.slice(2,6).every((n)=>n===0))return w(e);if(t[0]===8194)return w(`${t[1]>>8}.${t[1]&255}.${t[2]>>8}.${t[2]&255}`);return(t[0]&65024)===64512||(t[0]&65472)===65152||(t[0]&65280)===65280||t[0]===8193&&t[1]===3512||t[0]===8193&&t[1]<512||t[0]===256}function M(r){let t=r.replace(/^\[|\]$/g,""),e=A(t.split("%")[0]);if(e===4)return w(t);if(e===6)return mt(t);return!0}var ft=[".localhost",".local",".internal",".localdomain",".home.arpa"];function gt(r,t={}){let e;try{e=new URL(r)}catch{return{reason:"invalid",message:"Not a valid URL"}}if(!(t.allowHttp?["https:","http:"]:["https:"]).includes(e.protocol))return{reason:"scheme",message:t.allowHttp?"The URL must start with https:// or http://":"The URL must start with https://"};if(e.username||e.password)return{reason:"credentials",message:"The URL must not contain a username or password"};if(t.allowPrivate)return null;let s=e.hostname.replace(/^\[|\]$/g,"").toLowerCase().replace(/\.$/,"");if(!s)return{reason:"invalid",message:"Not a valid URL"};if(A(s.split("%")[0]))return M(s)?{reason:"private",message:"The URL points at a private or local address"}:null;if(s==="localhost"||!s.includes(".")||ft.some((o)=>s.endsWith(o)))return{reason:"private",message:"The URL points at a private or local address"};return null}async function lt(r){return(await ct(r,{all:!0,verbatim:!0})).map((e)=>e.address)}async function It(r,t={}){let e=gt(r,t);if(e)throw new p(e.message,e.reason);let n=new URL(r);if(t.allowPrivate)return n;let s=n.hostname.replace(/^\[|\]$/g,"");if(A(s.split("%")[0]))return n;let o;try{o=await(t.resolve??lt)(s)}catch{throw new p(`Could not resolve ${s}`,"unresolvable")}if(!o.length)throw new p(`Could not resolve ${s}`,"unresolvable");if(o.some(M))throw new p("The URL points at a private or local address","private");return n}export{c as InvalidWebhookSignature,p as OutboundUrlError,$t as _resetCryptCacheForTests,at as apiKeyHint,X as argon2Encode,It as assertPublicOutboundUrl,Pt as base64Encode,z as bcryptEncode,Ut as check,xt as decrypt,N as detectAlgorithm,bt as encrypt,_t as generateApiKey,Rt as generateAppKey,Tt as generateWebhookSecret,it as hashApiKey,Ut as hashCheck,N as hashDetectAlgorithm,Y as hashInfo,kt as hashMake,St as hashNeedsRehash,Y as info,M as isPrivateAddress,wt as legacyEncrypt,kt as make,kt as makeHash,St as needsRehash,gt as outboundUrlProblem,Ot as signWebhook,Bt as timingSafeEqualString,et as verifyGithub,Ut as verifyHash,nt as verifyHmac,rt as verifySignedWebhook,tt as verifyStripe,Ht as verifyWebhook};
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Whether an IP address is anything other than a public unicast address:
3
+ * loopback, private, carrier-grade NAT, link-local (cloud metadata),
4
+ * documentation, multicast, reserved, or IPv4 hidden in an IPv6 form. A
5
+ * string that is not an IP address counts as private, so a caller that forgot
6
+ * to resolve a name fails closed.
7
+ */
8
+ export declare function isPrivateAddress(address: string): boolean;
9
+ /**
10
+ * What is wrong with an outbound URL before any lookup: not a URL, a scheme
11
+ * other than https (and http when allowed), a username or password in it, or
12
+ * a host that is a private address or a local-only name. Null when nothing is.
13
+ * Pair it with {@link assertPublicOutboundUrl} at send time, since a public
14
+ * name can still resolve to a private address.
15
+ */
16
+ export declare function outboundUrlProblem(url: string, options?: OutboundUrlOptions): { reason: OutboundUrlReason, message: string } | null;
17
+ /**
18
+ * Check an outbound URL right before the request: {@link outboundUrlProblem},
19
+ * then every address its host resolves to must be public. Throws
20
+ * {@link OutboundUrlError}; returns the parsed URL.
21
+ *
22
+ * The request itself resolves the name again, so a host that answers with a
23
+ * public address here and a private one a moment later (DNS rebinding) is not
24
+ * fully covered; a deployment that needs that also blocks private ranges at
25
+ * its egress.
26
+ */
27
+ export declare function assertPublicOutboundUrl(url: string, options?: OutboundUrlOptions): Promise<URL>;
28
+ export declare interface OutboundUrlOptions {
29
+ allowHttp?: boolean
30
+ allowPrivate?: boolean
31
+ resolve?: (hostname: string) => Promise<string[]>
32
+ }
33
+ /**
34
+ * Guarding requests a server makes to URLs somebody else typed in: outgoing
35
+ * webhooks, "import from URL", link previews. Without a guard such a URL is
36
+ * a way into the server's own network (SSRF): `http://169.254.169.254/` is a
37
+ * cloud metadata service, `http://localhost:5432/` the database.
38
+ */
39
+ /** Why an outbound URL was refused. */
40
+ export type OutboundUrlReason = 'invalid' | 'scheme' | 'credentials' | 'private' | 'unresolvable';
41
+ /** Thrown by {@link assertPublicOutboundUrl}. */
42
+ export declare class OutboundUrlError extends Error {
43
+ readonly reason: OutboundUrlReason;
44
+ constructor(message: string, reason: OutboundUrlReason);
45
+ }
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.163",
5
+ "version": "0.75.164",
6
6
  "description": "The Stacks framework security.",
7
7
  "author": "Chris Breuer",
8
8
  "contributors": [
@@ -57,13 +57,13 @@
57
57
  "prepublishOnly": "bun run build"
58
58
  },
59
59
  "dependencies": {
60
- "@stacksjs/config": "0.75.163",
60
+ "@stacksjs/config": "0.75.164",
61
61
  "ts-security-crypto": "0.0.2"
62
62
  },
63
63
  "devDependencies": {
64
- "@stacksjs/env": "0.75.163",
65
- "@stacksjs/types": "0.75.163",
66
- "@stacksjs/validation": "0.75.163",
64
+ "@stacksjs/env": "0.75.164",
65
+ "@stacksjs/types": "0.75.164",
66
+ "@stacksjs/validation": "0.75.164",
67
67
  "better-dx": "^0.2.24"
68
68
  }
69
69
  }