Auto-commit 2026-04-29 16:31

This commit is contained in:
2026-04-29 16:31:27 -04:00
parent e8687bb6b2
commit 0495ee5bd2
19691 changed files with 3272886 additions and 138 deletions

View File

@@ -0,0 +1,20 @@
declare class RequestCanonicalizer {
method: string;
uri: string;
queryParams: Record<string, string>;
requestBody: any;
headers: Record<string, string>;
constructor(method: string, uri: string, queryParams: Record<string, string>, requestBody: any, headers: Record<string, string>);
getCanonicalizedMethod(): string;
customEncode(str: string): string;
ASCIICompare(a: string, b: string): number;
getCanonicalizedPath(): string;
getCanonicalizedQueryParams(): string;
getCanonicalizedHeaders(): string;
getCanonicalizedHashedHeaders(): string;
getCanonicalizedRequestBody(): string;
sha256Hex(body: string): string;
getCanonicalizedRequestString(): string;
create(): string;
}
export default RequestCanonicalizer;

View File

@@ -0,0 +1,97 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const crypto_1 = __importDefault(require("crypto"));
class RequestCanonicalizer {
constructor(method, uri, queryParams, requestBody, headers) {
this.method = method;
this.uri = uri;
this.queryParams = queryParams;
this.requestBody = requestBody;
this.headers = headers;
}
getCanonicalizedMethod() {
return this.method.toUpperCase();
}
customEncode(str) {
return encodeURIComponent(decodeURIComponent(str))
.replace(/\*/g, "%2A")
.replace(/%7E/g, "~");
}
ASCIICompare(a, b) {
if (a < b)
return -1;
return a > b ? 1 : 0;
}
getCanonicalizedPath() {
// Remove query string from path
const path = this.uri.split("?")[0];
// Normalize duplicate slashes (but preserve the leading one)
const normalizedPath = path.replace(/\/+/g, "/");
// We must preserve slashes (as path delimiters) but encode each segment
// Split and encode, but first decode each segment to avoid double-encoding
return normalizedPath
.split("/")
.map((segment) => this.customEncode(segment))
.join("/");
}
getCanonicalizedQueryParams() {
if (!this.queryParams) {
return "";
}
// sort query params on the basis of '{key}={value}'
const sortedQueryParams = Object.entries(this.queryParams)
.map(([key, value]) => {
return `${key}=${value}`;
})
.sort((a, b) => this.ASCIICompare(a, b)) // forces ASCII sorting using custom compare
.map((param) => {
const [key, value] = param.split("=");
return `${this.customEncode(key)}=${this.customEncode(value)}`; // encode and concatenate as `key=value`
});
return sortedQueryParams.join("&");
}
getCanonicalizedHeaders() {
// sort headers on the basis of '{key}:{value}'
const sortedHeaders = Object.keys(this.headers)
.map((key) => {
if (!this.headers[key]) {
return `${key.toLowerCase()}:`;
}
return `${key.toLowerCase()}:${this.headers[key].trim()}`;
})
.sort((a, b) => this.ASCIICompare(a, b)); // forces ASCII sorting using custom compare
return `${sortedHeaders.join("\n")}\n`;
}
getCanonicalizedHashedHeaders() {
const sortedHeaders = Object.keys(this.headers).sort((a, b) => this.ASCIICompare(a, b)); // forces ASCII sorting using custom compare
return sortedHeaders.join(";");
}
getCanonicalizedRequestBody() {
if (!this.requestBody) {
return "";
}
if (typeof this.requestBody === "string") {
return this.sha256Hex(this.requestBody);
}
else
return this.sha256Hex(JSON.stringify(this.requestBody));
}
sha256Hex(body) {
return crypto_1.default.createHash("sha256").update(body).digest("hex");
}
getCanonicalizedRequestString() {
return `${this.getCanonicalizedMethod()}
${this.getCanonicalizedPath()}
${this.getCanonicalizedQueryParams()}
${this.getCanonicalizedHeaders()}
${this.getCanonicalizedHashedHeaders()}
${this.getCanonicalizedRequestBody()}`;
}
create() {
return this.sha256Hex(this.getCanonicalizedRequestString());
}
}
exports.default = RequestCanonicalizer;

View File

@@ -0,0 +1,45 @@
import { ValidationClientOptions } from "../../base/ValidationClient";
import RequestCanonicalizer from "./RequestCanonicalizer";
import jwt, { Algorithm } from "jsonwebtoken";
declare class ValidationToken {
static readonly DEFAULT_ALGORITHM: "RS256";
static readonly ALGORITHMS: readonly [jwt.Algorithm, jwt.Algorithm];
private readonly _accountSid;
private readonly _credentialSid;
private readonly _signingKey;
private readonly _privateKey;
private readonly _algorithm;
ttl: number;
get accountSid(): string;
get credentialSid(): string;
get signingKey(): string;
get privateKey(): string;
get algorithm(): Algorithm;
/**
* @constructor
* @param opts - The Options used to configure the ValidationToken
* @param opts.accountSid - The account SID
* @param opts.credentialSid - The credential SID for public key submitted to Twilio
* @param opts.signingKey - The signing key
* @param opts.privateKey - The private key for signing the token
* @param opts.algorithm - The algorithm to use for signing the token
* @param opts.ttl - The time to live for the token in seconds
*/
constructor(opts: ValidationClientOptions);
/**
* Generates a `RequestCanonicalizer` instance for the given HTTP request.
*
* @param request - The HTTP request object containing details such as headers, URL, method, query parameters, and body.
* @throws {Error} If the request URL or method is missing.
* @returns {RequestCanonicalizer} - An instance of `RequestCanonicalizer` initialized with the canonicalized request details.
*/
getRequestCanonicalizer(request: any): RequestCanonicalizer;
/**
* Generate a JWT token to include in the request header for PKCV
* @param request - The request object
* @returns {string} - The JWT token
*/
fromHttpRequest(request: any): string;
}
declare namespace ValidationToken { }
export = ValidationToken;

View File

@@ -0,0 +1,121 @@
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const RequestCanonicalizer_1 = __importDefault(require("./RequestCanonicalizer"));
const jsonwebtoken_1 = __importDefault(require("jsonwebtoken"));
class ValidationToken {
get accountSid() {
return this._accountSid;
}
get credentialSid() {
return this._credentialSid;
}
get signingKey() {
return this._signingKey;
}
get privateKey() {
return this._privateKey;
}
get algorithm() {
return this._algorithm;
}
/**
* @constructor
* @param opts - The Options used to configure the ValidationToken
* @param opts.accountSid - The account SID
* @param opts.credentialSid - The credential SID for public key submitted to Twilio
* @param opts.signingKey - The signing key
* @param opts.privateKey - The private key for signing the token
* @param opts.algorithm - The algorithm to use for signing the token
* @param opts.ttl - The time to live for the token in seconds
*/
constructor(opts) {
if (!opts.accountSid) {
throw new Error("accountSid is required");
}
if (!opts.credentialSid) {
throw new Error("credentialSid is required");
}
if (!opts.signingKey) {
throw new Error("signingKey is required");
}
if (!opts.privateKey) {
throw new Error("privateKey is required");
}
const algorithm = opts.algorithm ?? ValidationToken.DEFAULT_ALGORITHM; // default to RS256;
if (!ValidationToken.ALGORITHMS.includes(algorithm)) {
throw new Error("Algorithm not supported. Allowed values are " +
ValidationToken.ALGORITHMS.join(", "));
}
this._accountSid = opts.accountSid;
this._credentialSid = opts.credentialSid;
this._signingKey = opts.signingKey;
this._privateKey = opts.privateKey;
this._algorithm = algorithm;
this.ttl = 300;
}
/**
* Generates a `RequestCanonicalizer` instance for the given HTTP request.
*
* @param request - The HTTP request object containing details such as headers, URL, method, query parameters, and body.
* @throws {Error} If the request URL or method is missing.
* @returns {RequestCanonicalizer} - An instance of `RequestCanonicalizer` initialized with the canonicalized request details.
*/
getRequestCanonicalizer(request) {
const headers = request.headers ?? {};
const requestUrl = request.url;
const method = request.method;
const queryParams = request.params;
const requestBody = request.data;
if (!requestUrl) {
throw new Error("Url is required");
}
if (!method) {
throw new Error("Method is required");
}
const url = new URL(requestUrl);
let signedHeaders = {
host: url.host,
authorization: headers["Authorization"],
};
return new RequestCanonicalizer_1.default(method, url.pathname, queryParams, requestBody, signedHeaders);
}
/**
* Generate a JWT token to include in the request header for PKCV
* @param request - The request object
* @returns {string} - The JWT token
*/
fromHttpRequest(request) {
try {
const requestCanonicalizer = this.getRequestCanonicalizer(request);
const canonicalizedRequest = requestCanonicalizer.create();
const header = {
cty: "twilio-pkrv;v=1",
typ: "JWT",
alg: this._algorithm,
kid: this._credentialSid,
};
const payload = {
iss: this._signingKey,
sub: this._accountSid,
hrh: requestCanonicalizer.getCanonicalizedHashedHeaders(),
rqh: canonicalizedRequest,
};
return jsonwebtoken_1.default.sign(payload, this._privateKey, {
header: header,
algorithm: this._algorithm,
expiresIn: this.ttl,
});
}
catch (err) {
throw new Error("Error generating JWT token " + err);
}
}
}
ValidationToken.DEFAULT_ALGORITHM = "RS256";
ValidationToken.ALGORITHMS = [
"RS256",
"PS256",
];
module.exports = ValidationToken;