@stacksjs/security 0.75.187 → 0.75.188
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/crypt.d.ts +0 -6
- package/dist/index.js +1 -1
- package/dist/outbound.d.ts +40 -5
- package/package.json +5 -5
package/dist/crypt.d.ts
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import { encrypt as cryptoEncrypt } from 'ts-security-crypto';
|
|
2
2
|
declare function encrypt(message: string, customPassphrase?: string): Promise<string>;
|
|
3
3
|
declare function decrypt(encrypted: string, customPassphrase?: string): Promise<string>;
|
|
4
|
-
/**
|
|
5
|
-
* For tests + observability: reset the per-process derived-key cache.
|
|
6
|
-
* Production code never needs to call this — the cache is bounded
|
|
7
|
-
* (`DERIVED_KEY_CACHE_MAX = 64`) and entries evict in FIFO order.
|
|
8
|
-
*/
|
|
9
|
-
export declare function _resetCryptCacheForTests(): void;
|
|
10
4
|
// Re-export pre-existing helpers from ts-security-crypto so callers
|
|
11
5
|
// that imported them from `@stacksjs/security` keep working.
|
|
12
6
|
export { decrypt, encrypt };
|
package/dist/index.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
// @bun
|
|
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};
|
|
2
|
+
var rt=import.meta.require;import{Buffer as y}from"buffer";import{config as F}from"@stacksjs/config";import{decrypt as B,encrypt as Ot}from"ts-security-crypto";function H(r){return r.startsWith("base64:")?r.slice(7):r}var L=1,M=2,nt="stacks:encrypt:v2",O=600000,P=32,m=16,l=12,st=64,p=new Map;function ot(r,t,e){return`${e}:${y.from(t).toString("base64")}:${r}`}async function _(r,t,e){let n=ot(r,t,e),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(r),{name:"PBKDF2"},!1,["deriveKey"]),i=await crypto.subtle.deriveKey({name:"PBKDF2",salt:new Uint8Array(t),iterations:e,hash:"SHA-256"},o,{name:"AES-GCM",length:P*8},!1,["encrypt","decrypt"]);if(p.set(n,{key:i,hits:1}),p.size>st){let a=p.keys().next().value;if(a!==void 0)p.delete(a)}return i}function I(r){if(!r.startsWith("base64:"))return null;let t=y.from(r.slice(7),"base64");return t.length>=P?new Uint8Array(t):null}async function V(r,t){let e=await crypto.subtle.importKey("raw",new Uint8Array(r),"HKDF",!1,["deriveKey"]);return await crypto.subtle.deriveKey({name:"HKDF",hash:"SHA-256",salt:new Uint8Array(t),info:new TextEncoder().encode(nt)},e,{name:"AES-GCM",length:P*8},!1,["encrypt","decrypt"])}async function it(r,t){let e=crypto.getRandomValues(new Uint8Array(m)),n=crypto.getRandomValues(new Uint8Array(l)),s=await V(t,e),o=new Uint8Array(await crypto.subtle.encrypt({name:"AES-GCM",iv:n},s,new TextEncoder().encode(r))),i=new Uint8Array(1+m+l+o.byteLength);return i[0]=M,i.set(e,1),i.set(n,1+m),i.set(o,1+m+l),y.from(i).toString("base64")}async function at(r,t){let e=y.from(r,"base64");if(e.length<1+m+l+16)return null;if(e[0]!==M)return null;let n=new Uint8Array(e.subarray(1,1+m)),s=new Uint8Array(e.subarray(1+m,1+m+l)),o=new Uint8Array(e.subarray(1+m+l)),i=await V(t,n);try{return new TextDecoder().decode(await crypto.subtle.decrypt({name:"AES-GCM",iv:s},i,o))}catch{return null}}async function ct(r,t){let e=crypto.getRandomValues(new Uint8Array(m)),n=crypto.getRandomValues(new Uint8Array(l)),s=await _(t,e,O),o=await crypto.subtle.encrypt({name:"AES-GCM",iv:n},s,new TextEncoder().encode(r)),i=new Uint8Array(o),a=new Uint8Array(1+m+l+i.byteLength);return a[0]=L,a.set(e,1),a.set(n,1+m),a.set(i,1+m+l),y.from(a).toString("base64")}async function ut(r,t){let e=y.from(r,"base64");if(e.length<1+m+l+16)return null;if(e[0]!==L)return null;let n=new Uint8Array(e.subarray(1,1+m)),s=new Uint8Array(e.subarray(1+m,1+m+l)),o=new Uint8Array(e.subarray(1+m+l)),i=await _(t,n,O);try{let a=await crypto.subtle.decrypt({name:"AES-GCM",iv:s},i,o);return new TextDecoder().decode(a)}catch{return null}}async function _t(r,t){if(!r&&r!=="")throw Error("encrypt() requires a string message");let e=t||F.app.key;if(!e)throw Error("APP_KEY is not defined");let n=I(e);if(n)return await it(r,n);return await ct(r,H(e))}async function It(r,t){if(!r)throw Error("decrypt() requires a non-empty encrypted string");let e=t||F.app.key;if(!e)throw Error("APP_KEY is not defined");let n=H(e),s=I(e);if(s){let i=await at(r,s);if(i!==null)return i}let o=await ut(r,n);if(o!==null)return o;try{return await B(r,n)}catch(i){if(n!==e)try{return await B(r,e)}catch{}throw i}}import{timingSafeEqual as D}from"crypto";import{base64Encode as Gt,hashPassword as G,verifyPassword as q}from"ts-security-crypto";var w=null;function R(){if(w)return w;try{let{hashing:r}=rt("@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 mt(r){let t=W(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+)/),i=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(i&&i[1]!==void 0)e.parallelism=Number.parseInt(i[1],10)}return{algorithm:t,options:e}}function Wt(r,t){let e=mt(r),n=R(),s=t?.algorithm||n.driver||"bcrypt",o=(c)=>{if(c==="argon2")return"argon2id";return c},i=o(e.algorithm),a=o(s);if(i!==a)return!0;if(i==="bcrypt"){let c=t?.rounds||n.bcrypt?.rounds||12;if(e.options.rounds!==c)return!0}if(i.startsWith("argon2")){let c=t?.memory||n.argon2?.memory||65536,u=t?.time||n.argon2?.time||2;if(e.options.memory!==c||e.options.rounds!==u)return!0}return!1}async function Yt(r,t){let e=R(),n=t?.algorithm||e.driver||"bcrypt";if(n==="bcrypt")return await ft(r,t?.rounds);if(n==="argon2"||n==="argon2id"||n==="argon2i"||n==="argon2d")return await lt(r,{type:n==="argon2"?"argon2id":n,memory:t?.memory,time:t?.time});throw Error(`Unsupported hashing algorithm: ${n}`)}async function zt(r,t){if(!r||!t)return!1;if(W(t)==="unknown")try{return await q(r,t)}catch{return!1}return await q(r,t)}async function ft(r,t){let e=R(),n=t||e.bcrypt?.rounds||12;return await G(r,{algorithm:"bcrypt",cost:n})}async function lt(r,t){let e=R(),n=t?.type||"argon2id",s=t?.memory||e.argon2?.memory||65536,o=t?.time||e.argon2?.time||2;return await G(r,{algorithm:n,memoryCost:s,timeCost:o})}function Xt(r,t){let e=Buffer.from(r),n=Buffer.from(t);if(e.length!==n.length)return D(e,e),!1;return D(e,n)}import{generateKey as gt}from"ts-security-crypto";function Qt(){return gt(32)}import{createHmac as ht,randomBytes as dt,timingSafeEqual as Y}from"crypto";class f extends Error{reason;constructor(r,t="mismatch"){super(r);this.name="InvalidWebhookSignature",this.reason=t}}function k(r){if(!/^[0-9a-f]*$/i.test(r)||r.length%2!==0)throw new f("Signature is not valid hex","malformed");return Buffer.from(r,"hex")}function T(r,t){if(r.length!==t.length)return Y(r,r),!1;return Y(r,t)}function S(r,t,e){return ht(e,r).update(t).digest()}function pt(r,t){let e=r.split(","),n=Number.NaN,s=[];for(let o of e){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 f(`${t} header is malformed`,"malformed");return{timestamp:n,signatures:s}}function z(r,t,e,n,s){if(!r)throw new f(`${s.name} secret is empty`,"missing");if(!t)throw new f(`${s.header} header is missing`,"missing");let{timestamp:o,signatures:i}=pt(t,s.header),a=n.toleranceSeconds??300,c=Math.floor((n.now??Date.now())/1000);if(Math.abs(c-o)>a)throw new f(`${s.name} timestamp is outside tolerance window`,"expired");let u=typeof e==="string"?e:e.toString("utf8"),g=S(r,`${o}.${u}`,"sha256");for(let b of i){let x;try{x=k(b)}catch{continue}if(T(g,x))return!0}throw new f(`${s.name} signature did not match`,"mismatch")}function yt(r,t,e,n={}){return z(r,t,e,n,{name:"Stripe webhook",header:"Stripe-Signature"})}function wt(r,t,e,n={}){return z(r,t,e,n,{name:"Webhook",header:"Signature"})}function ee(r,t,e={}){let n=(Array.isArray(r)?r:[r]).filter((a)=>typeof a==="string"&&a.length>0);if(!n.length)throw new f("Webhook signing secret is empty","missing");let s=e.timestamp??Math.floor(Date.now()/1000);if(!Number.isInteger(s)||s<0)throw new f("Webhook timestamp must be whole unix seconds","malformed");let o=typeof t==="string"?t:t.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 re(r="whsec_"){return`${r}${dt(32).toString("base64url")}`}function bt(r,t,e){if(!r)throw new f("GitHub webhook secret is empty","missing");if(!t)throw new f("X-Hub-Signature-256 header is missing","missing");let n="sha256=";if(!t.startsWith(n))throw new f("X-Hub-Signature-256 has wrong algorithm prefix","malformed");let s=k(t.slice(n.length)),o=S(r,e,"sha256");if(!T(o,s))throw new f("GitHub webhook signature did not match","mismatch");return!0}function xt(r,t,e,n="sha256"){if(!r)throw new f("HMAC secret is empty","missing");if(!e)throw new f("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,i=k(o),a=S(r,t,n);if(!T(a,i))throw new f("HMAC signature did not match","mismatch");return!0}function ne(r,t,e,n,s={}){switch(r){case"stripe":return yt(t,e,n,s);case"github":return bt(t,e,n);case"generic":return xt(t,n,e,s.algorithm??"sha256");case"signed":return wt(t,e,n,s);default:throw new f(`Unknown webhook provider: ${String(r)}`,"malformed")}}import{createHash as At,randomBytes as vt}from"crypto";function $t(r){return At("sha256").update(r,"utf8").digest("hex")}function Ut(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 ie(r="",t=32){if(!Number.isInteger(t)||t<16)throw Error("An API key needs at least 16 random bytes");let e=`${r}${vt(t).toString("base64url")}`;return{key:e,hash:$t(e),hint:Ut(e,r)}}import{lookup as Rt}from"dns/promises";import{isIP as A}from"net";class h extends Error{reason;constructor(r,t){super(r);this.name="OutboundUrlError",this.reason=t}}function j(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 E(r){let t=j(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 St(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 u=j(n[2]);if(!u)return null;t=`${n[1]}${(u[0]<<8|u[1]).toString(16)}:${(u[2]<<8|u[3]).toString(16)}`}let s=t.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 Et(r){let t=St(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 E(e);if(t[0]===100&&t[1]===65435&&t.slice(2,6).every((n)=>n===0))return E(e);if(t[0]===8194)return E(`${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 C(r){let t=r.replace(/^\[|\]$/g,""),e=A(t.split("%")[0]);if(e===4)return E(t);if(e===6)return Et(t);return!0}var Pt=[".localhost",".local",".internal",".localdomain",".home.arpa"];function kt(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 C(s)?{reason:"private",message:"The URL points at a private or local address"}:null;if(s==="localhost"||!s.includes(".")||Pt.some((o)=>s.endsWith(o)))return{reason:"private",message:"The URL points at a private or local address"};return null}async function J(r){return(await Rt(r,{all:!0,verbatim:!0})).map((e)=>e.address)}async function Tt(r,t={}){let e=kt(r,t);if(e)throw new h(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??J)(s)}catch{throw new h(`Could not resolve ${s}`,"unresolvable")}if(!o.length)throw new h(`Could not resolve ${s}`,"unresolvable");if(o.some(C))throw new h("The URL points at a private or local address","private");return n}async function Ct(r,t){let e=await Tt(r,t),n=e.hostname.replace(/^\[|\]$/g,"");if(A(n.split("%")[0])||t.allowPrivate)return{parsed:e,addresses:[]};let s=await(t.resolve??J)(n);return{parsed:e,addresses:s}}var Kt=new Set([301,302,303,307,308]),Nt=["authorization","cookie","proxy-authorization"];function Bt(r,t,e){if(!t)return[r.href,e];let n=new URL(r.href);n.hostname=A(t)===6?`[${t}]`:t;let s=new Headers(e.headers);s.set("Host",r.host);let o={...e,headers:s};if(r.protocol==="https:")o.tls={serverName:r.hostname.replace(/^\[|\]$/g,"")};return[n.href,o]}async function X(r,t){let e=Number(r.headers.get("content-length")??Number.NaN);if(Number.isFinite(e)&&e>t)throw new h(`The response is ${e} bytes, over the ${t} byte limit`,"too-large");if(!r.body)return r;let n=[],s=0,o=r.body.getReader();try{for(;;){let{done:c,value:u}=await o.read();if(c)break;if(s+=u.byteLength,s>t)throw await o.cancel(),new h(`The response is over the ${t} 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:r.status,statusText:r.statusText,headers:r.headers})}async function me(r,t={},e={}){let n=e.maxRedirects??3,s=e.maxBytes??5242880,o=e.timeoutMs??1e4,i=AbortSignal.timeout(o),a=t.signal?AbortSignal.any([t.signal,i]):i,c=t.body==null||typeof t.body==="string"||t.body instanceof Uint8Array||t.body instanceof ArrayBuffer,u=r,g={...t,signal:a,redirect:"manual"},b="";for(let x=0;;x++){let{parsed:v,addresses:Q}=await Ct(u,e);if(b&&v.origin!==b){let U=new Headers(g.headers);for(let et of Nt)U.delete(et);g={...g,headers:U}}b=v.origin;let[Z,tt]=Bt(v,Q.find((U)=>!C(U)),g),d=await fetch(Z,tt);if(!Kt.has(d.status))return await X(d,s);let K=d.headers.get("location");if(!K)return await X(d,s);if(x>=n)throw new h(`The URL redirected more than ${n} times`,"redirects");if(!c)throw new h("The URL redirected, and a streaming request body cannot be sent again","unreplayable-body");let N=(g.method??"GET").toUpperCase();if(d.status===303||(d.status===301||d.status===302)&&N!=="GET"&&N!=="HEAD")g={...g,method:"GET",body:void 0};u=new URL(K,v).href}}export{f as InvalidWebhookSignature,h as OutboundUrlError,Ut as apiKeyHint,lt as argon2Encode,Tt as assertPublicOutboundUrl,Gt as base64Encode,ft as bcryptEncode,zt as check,It as decrypt,W as detectAlgorithm,_t as encrypt,me as fetchPublicUrl,ie as generateApiKey,Qt as generateAppKey,re as generateWebhookSecret,$t as hashApiKey,zt as hashCheck,W as hashDetectAlgorithm,mt as hashInfo,Yt as hashMake,Wt as hashNeedsRehash,mt as info,C as isPrivateAddress,Ot as legacyEncrypt,Yt as make,Yt as makeHash,Wt as needsRehash,kt as outboundUrlProblem,ee as signWebhook,Xt as timingSafeEqualString,bt as verifyGithub,zt as verifyHash,xt as verifyHmac,wt as verifySignedWebhook,yt as verifyStripe,ne as verifyWebhook};
|
package/dist/outbound.d.ts
CHANGED
|
@@ -19,17 +19,52 @@ export declare function outboundUrlProblem(url: string, options?: OutboundUrlOpt
|
|
|
19
19
|
* then every address its host resolves to must be public. Throws
|
|
20
20
|
* {@link OutboundUrlError}; returns the parsed URL.
|
|
21
21
|
*
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
22
|
+
* **Blessing a URL is not the same as making a safe request, and this only
|
|
23
|
+
* does the first.** A caller that takes the returned URL and fetches it gives
|
|
24
|
+
* the guarantee straight back (stacksjs/stacks#2909):
|
|
25
|
+
*
|
|
26
|
+
* - `fetch` defaults to `redirect: 'follow'`, so a validated public host that
|
|
27
|
+
* answers `302 Location: http://169.254.169.254/...` is followed, and
|
|
28
|
+
* nothing checks the second request. Pass `redirect: 'manual'` and
|
|
29
|
+
* re-validate every hop, or the whole guard is one redirect wide.
|
|
30
|
+
* - `fetch` resolves the name again, so an address checked here is not the
|
|
31
|
+
* address connected to. A host answering public once and private a moment
|
|
32
|
+
* later (DNS rebinding) passes.
|
|
33
|
+
*
|
|
34
|
+
* Prefer {@link fetchPublicUrl}, which makes the request itself and handles
|
|
35
|
+
* both. Use this one when the check alone is what you want - validating a
|
|
36
|
+
* webhook URL at the moment someone saves it, say.
|
|
26
37
|
*/
|
|
27
38
|
export declare function assertPublicOutboundUrl(url: string, options?: OutboundUrlOptions): Promise<URL>;
|
|
39
|
+
/**
|
|
40
|
+
* Fetch a URL somebody else supplied, checking every hop rather than the
|
|
41
|
+
* first one (stacksjs/stacks#2909).
|
|
42
|
+
*
|
|
43
|
+
* {@link assertPublicOutboundUrl} blesses a URL, and the caller's `fetch`
|
|
44
|
+
* then follows redirects and re-resolves the name, which hands back
|
|
45
|
+
* everything the check established. This makes the request itself:
|
|
46
|
+
*
|
|
47
|
+
* - every hop is validated by the same rules, up to `maxRedirects`
|
|
48
|
+
* - the connection goes to an address that was checked, with the name in
|
|
49
|
+
* `Host` and in TLS SNI, so a rebinding answer cannot be substituted
|
|
50
|
+
* - `Authorization`, `Cookie` and `Proxy-Authorization` are dropped when a
|
|
51
|
+
* redirect crosses to another origin
|
|
52
|
+
* - the body is capped and the whole chain is bounded by a timeout
|
|
53
|
+
*
|
|
54
|
+
* A streaming request body cannot be replayed onto a redirect, so a redirect
|
|
55
|
+
* is refused rather than silently sent without it.
|
|
56
|
+
*/
|
|
57
|
+
export declare function fetchPublicUrl(url: string, init?: RequestInit, options?: PublicFetchOptions): Promise<Response>;
|
|
28
58
|
export declare interface OutboundUrlOptions {
|
|
29
59
|
allowHttp?: boolean
|
|
30
60
|
allowPrivate?: boolean
|
|
31
61
|
resolve?: (hostname: string) => Promise<string[]>
|
|
32
62
|
}
|
|
63
|
+
export declare interface PublicFetchOptions extends OutboundUrlOptions {
|
|
64
|
+
maxRedirects?: number
|
|
65
|
+
timeoutMs?: number
|
|
66
|
+
maxBytes?: number
|
|
67
|
+
}
|
|
33
68
|
/**
|
|
34
69
|
* Guarding requests a server makes to URLs somebody else typed in: outgoing
|
|
35
70
|
* webhooks, "import from URL", link previews. Without a guard such a URL is
|
|
@@ -37,7 +72,7 @@ export declare interface OutboundUrlOptions {
|
|
|
37
72
|
* cloud metadata service, `http://localhost:5432/` the database.
|
|
38
73
|
*/
|
|
39
74
|
/** Why an outbound URL was refused. */
|
|
40
|
-
export type OutboundUrlReason = 'invalid' | 'scheme' | 'credentials' | 'private' | 'unresolvable';
|
|
75
|
+
export type OutboundUrlReason = 'invalid' | 'scheme' | 'credentials' | 'private' | 'unresolvable' | 'redirects' | 'too-large' | 'unreplayable-body';
|
|
41
76
|
/** Thrown by {@link assertPublicOutboundUrl}. */
|
|
42
77
|
export declare class OutboundUrlError extends Error {
|
|
43
78
|
readonly reason: OutboundUrlReason;
|
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.
|
|
5
|
+
"version": "0.75.188",
|
|
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.
|
|
60
|
+
"@stacksjs/config": "0.75.188",
|
|
61
61
|
"ts-security-crypto": "0.0.2"
|
|
62
62
|
},
|
|
63
63
|
"devDependencies": {
|
|
64
|
-
"@stacksjs/env": "0.75.
|
|
65
|
-
"@stacksjs/types": "0.75.
|
|
66
|
-
"@stacksjs/validation": "0.75.
|
|
64
|
+
"@stacksjs/env": "0.75.188",
|
|
65
|
+
"@stacksjs/types": "0.75.188",
|
|
66
|
+
"@stacksjs/validation": "0.75.188",
|
|
67
67
|
"better-dx": "^0.2.24"
|
|
68
68
|
}
|
|
69
69
|
}
|