@stacksjs/email 0.70.302 → 0.70.304
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/unsubscribe.d.ts +4 -3
- package/dist/unsubscribe.js +1 -1
- package/package.json +5 -5
package/dist/unsubscribe.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* works months later, short enough that a leaked URL doesn't grant
|
|
5
5
|
* indefinite control. Tighter caps available via `ttlSeconds`.
|
|
6
6
|
*/
|
|
7
|
-
export declare function createUnsubscribeToken(email: string, ttlSeconds?: number): string;
|
|
7
|
+
export declare function createUnsubscribeToken(email: string, ttlSeconds?: number, scope?: string): string;
|
|
8
8
|
/**
|
|
9
9
|
* Verify a signed unsubscribe token. Returns the email + a
|
|
10
10
|
* discriminated outcome — callers map invalid results to a 400/410
|
|
@@ -21,7 +21,7 @@ export declare function verifyUnsubscribeToken(token: string): UnsubscribeVerifi
|
|
|
21
21
|
* see {@link buildListUnsubscribeHeaders} for RFC 8058
|
|
22
22
|
* (one-click) compatibility.
|
|
23
23
|
*/
|
|
24
|
-
export declare function buildUnsubscribeUrl(email: string, ttlSeconds?: number, options?: { baseUrl?: string, routePrefix?: string }): string;
|
|
24
|
+
export declare function buildUnsubscribeUrl(email: string, ttlSeconds?: number, options?: { baseUrl?: string, routePrefix?: string, scope?: string }): string;
|
|
25
25
|
/**
|
|
26
26
|
* Build a `List-Unsubscribe` / `List-Unsubscribe-Post` header
|
|
27
27
|
* pair (RFC 8058). Gmail/Apple Mail use these for the native
|
|
@@ -31,9 +31,10 @@ export declare function buildUnsubscribeUrl(email: string, ttlSeconds?: number,
|
|
|
31
31
|
* Returns a map suitable for passing into `EmailMessage.headers`
|
|
32
32
|
* (or merging with existing headers).
|
|
33
33
|
*/
|
|
34
|
-
export declare function buildListUnsubscribeHeaders(email: string, ttlSeconds?: number, options?: { baseUrl?: string, routePrefix?: string }): Record<string, string>;
|
|
34
|
+
export declare function buildListUnsubscribeHeaders(email: string, ttlSeconds?: number, options?: { baseUrl?: string, routePrefix?: string, scope?: string }): Record<string, string>;
|
|
35
35
|
export declare interface UnsubscribeVerification {
|
|
36
36
|
valid: boolean
|
|
37
37
|
reason?: 'malformed' | 'bad_signature' | 'expired'
|
|
38
38
|
email?: string
|
|
39
|
+
scope?: string
|
|
39
40
|
}
|
package/dist/unsubscribe.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
import{createHmac,timingSafeEqual}from"node:crypto";import process from"node:process";import{Buffer}from"node:buffer";const DEFAULT_TTL_SECONDS=2592000,DEFAULT_ROUTE="/_stacks/email/unsubscribe";function getAppKey(){const k=process.env.APP_KEY;if(!k||k.length<16){if(process.env.APP_ENV==="production"||process.env.NODE_ENV==="production")throw Error("[email/unsubscribe] APP_KEY is missing or too short (need \u226516 chars). Cannot sign unsubscribe URL.")}return k||"stacks-default-key-dev-only-do-not-use-prod"}function b64UrlEncode(buf){return buf.toString("base64url")}function b64UrlDecode(s){return Buffer.from(s,"base64url")}export function createUnsubscribeToken(email,ttlSeconds=DEFAULT_TTL_SECONDS){if(!email)throw Error("[email/unsubscribe] email is required");const exp=Math.floor(Date.now()/1000)+Math.floor(ttlSeconds),claims={email:String(email).trim().toLowerCase(),exp,iss:"stacks"},payload=b64UrlEncode(Buffer.from(JSON.stringify(claims))),sig=b64UrlEncode(createHmac("sha256",getAppKey()).update(payload).digest());return`${payload}.${sig}`}export function verifyUnsubscribeToken(token){if(typeof token!=="string")return{valid:!1,reason:"malformed"};const parts=token.split(".");if(parts.length!==2)return{valid:!1,reason:"malformed"};const[payload,sig]=parts,expectedSig=createHmac("sha256",getAppKey()).update(payload).digest();let provided;try{provided=b64UrlDecode(sig)}catch{return{valid:!1,reason:"malformed"}}if(provided.length!==expectedSig.length||!timingSafeEqual(provided,expectedSig))return{valid:!1,reason:"bad_signature"};let claims;try{claims=JSON.parse(b64UrlDecode(payload).toString("utf8"))}catch{return{valid:!1,reason:"malformed"}}if(typeof claims.exp!=="number"||Math.floor(Date.now()/1000)>=claims.exp)return{valid:!1,reason:"expired"};if(!claims.email||typeof claims.email!=="string")return{valid:!1,reason:"malformed"};return{valid:!0,email:claims.email}}export function buildUnsubscribeUrl(email,ttlSeconds,options={}){const token=createUnsubscribeToken(email,ttlSeconds),base=(options.baseUrl||process.env.APP_URL||"http://localhost").replace(/\/$/,""),route=(options.routePrefix||DEFAULT_ROUTE).replace(/\/$/,"");return`${base}${route}/${token}`}export function buildListUnsubscribeHeaders(email,ttlSeconds,options){return{"List-Unsubscribe":`<${buildUnsubscribeUrl(email,ttlSeconds,options)}>`,"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}}
|
|
1
|
+
import{createHmac,timingSafeEqual}from"node:crypto";import process from"node:process";import{Buffer}from"node:buffer";const DEFAULT_TTL_SECONDS=2592000,DEFAULT_ROUTE="/_stacks/email/unsubscribe";function getAppKey(){const k=process.env.APP_KEY;if(!k||k.length<16){if(process.env.APP_ENV==="production"||process.env.NODE_ENV==="production")throw Error("[email/unsubscribe] APP_KEY is missing or too short (need \u226516 chars). Cannot sign unsubscribe URL.")}return k||"stacks-default-key-dev-only-do-not-use-prod"}function b64UrlEncode(buf){return buf.toString("base64url")}function b64UrlDecode(s){return Buffer.from(s,"base64url")}export function createUnsubscribeToken(email,ttlSeconds=DEFAULT_TTL_SECONDS,scope){if(!email)throw Error("[email/unsubscribe] email is required");const exp=Math.floor(Date.now()/1000)+Math.floor(ttlSeconds),claims={email:String(email).trim().toLowerCase(),exp,iss:"stacks",...scope?{scope:String(scope)}:{}},payload=b64UrlEncode(Buffer.from(JSON.stringify(claims))),sig=b64UrlEncode(createHmac("sha256",getAppKey()).update(payload).digest());return`${payload}.${sig}`}export function verifyUnsubscribeToken(token){if(typeof token!=="string")return{valid:!1,reason:"malformed"};const parts=token.split(".");if(parts.length!==2)return{valid:!1,reason:"malformed"};const[payload,sig]=parts,expectedSig=createHmac("sha256",getAppKey()).update(payload).digest();let provided;try{provided=b64UrlDecode(sig)}catch{return{valid:!1,reason:"malformed"}}if(provided.length!==expectedSig.length||!timingSafeEqual(provided,expectedSig))return{valid:!1,reason:"bad_signature"};let claims;try{claims=JSON.parse(b64UrlDecode(payload).toString("utf8"))}catch{return{valid:!1,reason:"malformed"}}if(typeof claims.exp!=="number"||Math.floor(Date.now()/1000)>=claims.exp)return{valid:!1,reason:"expired"};if(!claims.email||typeof claims.email!=="string")return{valid:!1,reason:"malformed"};return{valid:!0,email:claims.email,...typeof claims.scope==="string"&&claims.scope?{scope:claims.scope}:{}}}export function buildUnsubscribeUrl(email,ttlSeconds,options={}){const token=createUnsubscribeToken(email,ttlSeconds,options.scope),base=(options.baseUrl||process.env.APP_URL||"http://localhost").replace(/\/$/,""),route=(options.routePrefix||DEFAULT_ROUTE).replace(/\/$/,"");return`${base}${route}/${token}`}export function buildListUnsubscribeHeaders(email,ttlSeconds,options){return{"List-Unsubscribe":`<${buildUnsubscribeUrl(email,ttlSeconds,options)}>`,"List-Unsubscribe-Post":"List-Unsubscribe=One-Click"}}
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@stacksjs/email",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"sideEffects": false,
|
|
5
|
-
"version": "0.70.
|
|
5
|
+
"version": "0.70.304",
|
|
6
6
|
"description": "The Stacks Email integration. Painlessly create & manage your inboxes, templates, and send emails.",
|
|
7
7
|
"author": "Chris Breuer",
|
|
8
8
|
"contributors": [
|
|
@@ -63,10 +63,10 @@
|
|
|
63
63
|
"@stacksjs/ts-cloud": "^0.7.103"
|
|
64
64
|
},
|
|
65
65
|
"devDependencies": {
|
|
66
|
-
"@stacksjs/cli": "0.70.
|
|
67
|
-
"@stacksjs/config": "0.70.
|
|
66
|
+
"@stacksjs/cli": "0.70.304",
|
|
67
|
+
"@stacksjs/config": "0.70.304",
|
|
68
68
|
"better-dx": "^0.2.17",
|
|
69
|
-
"@stacksjs/error-handling": "0.70.
|
|
70
|
-
"@stacksjs/types": "0.70.
|
|
69
|
+
"@stacksjs/error-handling": "0.70.304",
|
|
70
|
+
"@stacksjs/types": "0.70.304"
|
|
71
71
|
}
|
|
72
72
|
}
|