@contentful/node-apps-toolkit
Advanced tools
Comparing version 1.3.2 to 1.4.0
export { signRequest } from './sign-request'; | ||
export { verifyRequest } from './verify-request'; | ||
export { ContentfulHeader } from './typings'; | ||
export { ContentfulHeader, ContentfulUserIdHeader, ContentfulAppIdHeader } from './typings'; | ||
export type { CanonicalRequest, SignedRequestHeaders } from './typings'; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.ContentfulHeader = exports.verifyRequest = exports.signRequest = void 0; | ||
exports.ContentfulAppIdHeader = exports.ContentfulUserIdHeader = exports.ContentfulHeader = exports.verifyRequest = exports.signRequest = void 0; | ||
var sign_request_1 = require("./sign-request"); | ||
@@ -10,2 +10,4 @@ Object.defineProperty(exports, "signRequest", { enumerable: true, get: function () { return sign_request_1.signRequest; } }); | ||
Object.defineProperty(exports, "ContentfulHeader", { enumerable: true, get: function () { return typings_1.ContentfulHeader; } }); | ||
Object.defineProperty(exports, "ContentfulUserIdHeader", { enumerable: true, get: function () { return typings_1.ContentfulUserIdHeader; } }); | ||
Object.defineProperty(exports, "ContentfulAppIdHeader", { enumerable: true, get: function () { return typings_1.ContentfulAppIdHeader; } }); | ||
//# sourceMappingURL=index.js.map |
@@ -1,51 +0,7 @@ | ||
import { CanonicalRequest, Secret, SignedRequestHeaders, Timestamp } from './typings'; | ||
import { CanonicalRequest, Secret, SignedRequestHeaders, Timestamp, ContextHeaders } from './typings'; | ||
/** | ||
* Given a secret, a canonical request and a timestamp, generates a signature. | ||
* It can be used to verify canonical requests to assess authenticity of the | ||
* sender and integrity of the payload. | ||
* | ||
* Given a secret, a canonical request, a timestamp and context headers, generates a signature. | ||
* ~~~ | ||
* const {signRequest, ContentfulHeader} = require('@contentful/node-apps-toolkit') | ||
* const {pick} = require('lodash') | ||
* const {server} = require('./imaginary-server') | ||
* | ||
* const SECRET = process.env.SECRET | ||
* | ||
* server.post('/api/my-resources', (req, res) => { | ||
* const incomingSignature = req.headers['x-contentful-signature'] | ||
* const incomingTimestamp = Number.parseInt(req.headers['x-contentful-timestamp']) | ||
* const incomingSignedHeaders = req.headers['x-contentful-signed-headers'] | ||
* const now = Date.now() | ||
* | ||
* if (!incomingSignature) { | ||
* res.send(400, 'Missing signature') | ||
* } | ||
* | ||
* if (now - incomingTimestamp > 1000) { | ||
* res.send(408, 'Request too old') | ||
* } | ||
* | ||
* const signedHeaders = incomingSignedHeaders.split(',') | ||
* | ||
* const {[ContentfulHeader.Signature]: computedSignature} = signRequest( | ||
* SECRET, | ||
* { | ||
* method: req.method, | ||
* path: req.url, | ||
* headers: pick(req.headers, signedHeaders), | ||
* body: JSON.stringify(req.body) | ||
* }, | ||
* incomingTimestamp | ||
* ) | ||
* | ||
* if (computedSignature !== incomingSignature) { | ||
* res.send(403, 'Invalid signature') | ||
* } | ||
* | ||
* // rest of the code | ||
* }) | ||
* | ||
* ~~~ | ||
* @category Requests | ||
*/ | ||
export declare const signRequest: (rawSecret: Secret, rawCanonicalRequest: CanonicalRequest, rawTimestamp?: Timestamp) => SignedRequestHeaders; | ||
export declare const signRequest: (rawSecret: Secret, rawCanonicalRequest: CanonicalRequest, rawTimestamp?: Timestamp, rawContextHeaders?: ContextHeaders) => SignedRequestHeaders; |
@@ -36,52 +36,18 @@ "use strict"; | ||
}; | ||
const getSubjectHeaders = (contextHeaders) => { | ||
let headers = {}; | ||
if (contextHeaders[typings_1.ContentfulUserIdHeader]) { | ||
headers[typings_1.ContentfulUserIdHeader] = contextHeaders[typings_1.ContentfulUserIdHeader]; | ||
} | ||
else if (contextHeaders[typings_1.ContentfulAppIdHeader]) { | ||
headers[typings_1.ContentfulAppIdHeader] = contextHeaders[typings_1.ContentfulAppIdHeader]; | ||
} | ||
return headers; | ||
}; | ||
/** | ||
* Given a secret, a canonical request and a timestamp, generates a signature. | ||
* It can be used to verify canonical requests to assess authenticity of the | ||
* sender and integrity of the payload. | ||
* | ||
* Given a secret, a canonical request, a timestamp and context headers, generates a signature. | ||
* ~~~ | ||
* const {signRequest, ContentfulHeader} = require('@contentful/node-apps-toolkit') | ||
* const {pick} = require('lodash') | ||
* const {server} = require('./imaginary-server') | ||
* | ||
* const SECRET = process.env.SECRET | ||
* | ||
* server.post('/api/my-resources', (req, res) => { | ||
* const incomingSignature = req.headers['x-contentful-signature'] | ||
* const incomingTimestamp = Number.parseInt(req.headers['x-contentful-timestamp']) | ||
* const incomingSignedHeaders = req.headers['x-contentful-signed-headers'] | ||
* const now = Date.now() | ||
* | ||
* if (!incomingSignature) { | ||
* res.send(400, 'Missing signature') | ||
* } | ||
* | ||
* if (now - incomingTimestamp > 1000) { | ||
* res.send(408, 'Request too old') | ||
* } | ||
* | ||
* const signedHeaders = incomingSignedHeaders.split(',') | ||
* | ||
* const {[ContentfulHeader.Signature]: computedSignature} = signRequest( | ||
* SECRET, | ||
* { | ||
* method: req.method, | ||
* path: req.url, | ||
* headers: pick(req.headers, signedHeaders), | ||
* body: JSON.stringify(req.body) | ||
* }, | ||
* incomingTimestamp | ||
* ) | ||
* | ||
* if (computedSignature !== incomingSignature) { | ||
* res.send(403, 'Invalid signature') | ||
* } | ||
* | ||
* // rest of the code | ||
* }) | ||
* | ||
* ~~~ | ||
* @category Requests | ||
*/ | ||
exports.signRequest = (rawSecret, rawCanonicalRequest, rawTimestamp = Date.now()) => { | ||
exports.signRequest = (rawSecret, rawCanonicalRequest, rawTimestamp = Date.now(), rawContextHeaders = {}) => { | ||
var _a; | ||
@@ -95,3 +61,5 @@ const canonicalRequest = typings_2.CanonicalRequestValidator.check(rawCanonicalRequest); | ||
const body = (_a = canonicalRequest.body) !== null && _a !== void 0 ? _a : ''; | ||
const { sortedHeaders, signedHeaders } = getSortedAndSignedHeaders(headers, timestamp); | ||
const contextHeaders = utils_1.normalizeContextHeaders(rawContextHeaders); | ||
const subject = getSubjectHeaders(contextHeaders); | ||
const { sortedHeaders, signedHeaders } = getSortedAndSignedHeaders({ ...headers, ...contextHeaders }, timestamp); | ||
return { | ||
@@ -101,4 +69,7 @@ [typings_1.ContentfulHeader.Signature]: hash({ method, headers: sortedHeaders, path, body }, secret), | ||
[typings_1.ContentfulHeader.Timestamp]: timestamp.toString(), | ||
[typings_1.ContentfulHeader.SpaceId]: rawContextHeaders.spaceId, | ||
[typings_1.ContentfulHeader.EnvironmentId]: rawContextHeaders.envId, | ||
...subject, | ||
}; | ||
}; | ||
//# sourceMappingURL=sign-request.js.map |
import * as runtypes from 'runtypes'; | ||
import { XOR } from 'ts-xor'; | ||
export declare enum ContentfulHeader { | ||
Timestamp = "x-contentful-timestamp", | ||
SignedHeaders = "x-contentful-signed-headers", | ||
Signature = "x-contentful-signature" | ||
Signature = "x-contentful-signature", | ||
SpaceId = "x-contentful-space-id", | ||
EnvironmentId = "x-contentful-environment-id" | ||
} | ||
export declare const ContentfulUserIdHeader = "x-contentful-user-id"; | ||
export declare const ContentfulAppIdHeader = "x-contentful-app-id"; | ||
declare type ContentfulHeaderWithApp = { | ||
[ContentfulAppIdHeader]: string; | ||
}; | ||
declare type ContentfulHeaderWithUser = { | ||
[ContentfulUserIdHeader]: string; | ||
}; | ||
export declare const CanonicalRequestValidator: runtypes.Intersect2<runtypes.Record<{ | ||
@@ -33,4 +44,15 @@ method: runtypes.Union7<runtypes.Literal<"GET">, runtypes.Literal<"PATCH">, runtypes.Literal<"HEAD">, runtypes.Literal<"POST">, runtypes.Literal<"DELETE">, runtypes.Literal<"OPTIONS">, runtypes.Literal<"PUT">>; | ||
}; | ||
export declare type Subject = Partial<XOR<{ | ||
appId: string; | ||
}, { | ||
userId: string; | ||
}>>; | ||
export declare type SubjectHeader = Partial<XOR<ContentfulHeaderWithApp, ContentfulHeaderWithUser>>; | ||
export declare type ContextHeaders = { | ||
spaceId: string; | ||
envId: string; | ||
} & Subject; | ||
export declare type SignedRequestHeaders = { | ||
[key in ContentfulHeader]: string; | ||
}; | ||
} & (SubjectHeader | {}); | ||
export {}; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.TimeToLiveValidator = exports.RequestMetadataValidator = exports.TimestampValidator = exports.SecretValidator = exports.CanonicalRequestValidator = exports.ContentfulHeader = void 0; | ||
exports.TimeToLiveValidator = exports.RequestMetadataValidator = exports.TimestampValidator = exports.SecretValidator = exports.CanonicalRequestValidator = exports.ContentfulAppIdHeader = exports.ContentfulUserIdHeader = exports.ContentfulHeader = void 0; | ||
// Remove when this eslint rule covers all the cases | ||
@@ -13,3 +13,7 @@ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/ROADMAP.md | ||
ContentfulHeader["Signature"] = "x-contentful-signature"; | ||
ContentfulHeader["SpaceId"] = "x-contentful-space-id"; | ||
ContentfulHeader["EnvironmentId"] = "x-contentful-environment-id"; | ||
})(ContentfulHeader = exports.ContentfulHeader || (exports.ContentfulHeader = {})); | ||
exports.ContentfulUserIdHeader = 'x-contentful-user-id'; | ||
exports.ContentfulAppIdHeader = 'x-contentful-app-id'; | ||
const MethodValidator = runtypes.Union(runtypes.Literal('GET'), runtypes.Literal('PATCH'), runtypes.Literal('HEAD'), runtypes.Literal('POST'), runtypes.Literal('DELETE'), runtypes.Literal('OPTIONS'), runtypes.Literal('PUT')); | ||
@@ -16,0 +20,0 @@ const PathValidator = runtypes.String.withConstraint((s) => s.startsWith('/'), { |
@@ -0,1 +1,2 @@ | ||
import { ContextHeaders, SignedRequestHeaders } from './typings'; | ||
export declare const getNormalizedEncodedURI: (uri: string) => string; | ||
@@ -9,2 +10,3 @@ export declare const sortHeaderKeys: (keyA: string, keyB: string) => 1 | -1; | ||
}; | ||
export declare const normalizeContextHeaders: (rawContextHeaders: ContextHeaders) => Partial<SignedRequestHeaders>; | ||
export declare const filter: <T = string>(obj: Record<string, any>, callback: (entry: [string, T]) => boolean) => { | ||
@@ -11,0 +13,0 @@ [k: string]: any; |
"use strict"; | ||
Object.defineProperty(exports, "__esModule", { value: true }); | ||
exports.map = exports.filter = exports.pickHeaders = exports.normalizeHeaders = exports.sortHeaderKeys = exports.getNormalizedEncodedURI = void 0; | ||
exports.map = exports.filter = exports.normalizeContextHeaders = exports.pickHeaders = exports.normalizeHeaders = exports.sortHeaderKeys = exports.getNormalizedEncodedURI = void 0; | ||
const querystring = require("querystring"); | ||
const typings_1 = require("./typings"); | ||
exports.getNormalizedEncodedURI = (uri) => { | ||
@@ -23,2 +24,18 @@ const [pathname, search] = uri.split('?'); | ||
}; | ||
const contextHeadersMap = { | ||
spaceId: typings_1.ContentfulHeader.SpaceId, | ||
envId: typings_1.ContentfulHeader.EnvironmentId, | ||
appId: typings_1.ContentfulAppIdHeader, | ||
userId: typings_1.ContentfulUserIdHeader, | ||
}; | ||
exports.normalizeContextHeaders = (rawContextHeaders) => { | ||
return Object.keys(rawContextHeaders).reduce((acc, curr) => { | ||
var _a; | ||
if (contextHeadersMap[curr]) { | ||
let key = contextHeadersMap[curr]; | ||
acc[key] = (_a = acc[key]) !== null && _a !== void 0 ? _a : rawContextHeaders[curr]; | ||
} | ||
return acc; | ||
}, {}); | ||
}; | ||
// Remove when this eslint rule covers all the cases | ||
@@ -25,0 +42,0 @@ // https://github.com/typescript-eslint/typescript-eslint/blob/master/packages/eslint-plugin/ROADMAP.md |
import { CanonicalRequest, Secret, TimeToLive } from './typings'; | ||
/** | ||
* Given a secret verifies a CanonicalRequest. Throws when signature is older than `rawTimeToLive` seconds. | ||
* Given a secret verifies a CanonicalRequest. It also throws when signature is older than `rawTimeToLive` seconds. | ||
* Pass `rawTimeToLive = 0` to disable TTL checks. | ||
@@ -5,0 +5,0 @@ * |
@@ -19,3 +19,3 @@ "use strict"; | ||
/** | ||
* Given a secret verifies a CanonicalRequest. Throws when signature is older than `rawTimeToLive` seconds. | ||
* Given a secret verifies a CanonicalRequest. It also throws when signature is older than `rawTimeToLive` seconds. | ||
* Pass `rawTimeToLive = 0` to disable TTL checks. | ||
@@ -22,0 +22,0 @@ * |
@@ -8,3 +8,3 @@ "use strict"; | ||
const exceptions_1 = require("./exceptions"); | ||
const makeIncomingRequest = ({ path = '/api/v1/resources/1', method = 'GET', headers, body, }, now = Date.now()) => { | ||
const makeIncomingRequest = ({ path = '/api/v1/resources/1', method = 'GET', headers, body, }, now = Date.now(), subject = {}) => { | ||
const request = { | ||
@@ -16,3 +16,8 @@ path, | ||
}; | ||
const signedHeaders = sign_request_1.signRequest(VALID_SECRET, request, now); | ||
const contextHeaders = { | ||
spaceId: 'my-space', | ||
envId: 'my-environment', | ||
...subject, | ||
}; | ||
const signedHeaders = sign_request_1.signRequest(VALID_SECRET, request, now, contextHeaders); | ||
return { | ||
@@ -37,2 +42,20 @@ ...request, | ||
}); | ||
it('verifies a verified request with user-id', () => { | ||
const now = Date.now(); | ||
const incomingRequest = makeIncomingRequest({ | ||
headers: { | ||
Authorization: 'Bearer TOKEN', | ||
}, | ||
}, now, { userId: 'my-user' }); | ||
assert(verify_request_1.verifyRequest(VALID_SECRET, incomingRequest, 0)); | ||
}); | ||
it('verifies a verified request with app-id', () => { | ||
const now = Date.now(); | ||
const incomingRequest = makeIncomingRequest({ | ||
headers: { | ||
Authorization: 'Bearer TOKEN', | ||
}, | ||
}, now, { appId: 'my-app' }); | ||
assert(verify_request_1.verifyRequest(VALID_SECRET, incomingRequest, 0)); | ||
}); | ||
describe('with time to live', () => { | ||
@@ -60,3 +83,5 @@ it('throws if request is too old', () => { | ||
it('verifies correctly with keys with different casing', () => { | ||
const incomingRequest = makeIncomingRequest({}); | ||
const incomingRequest = makeIncomingRequest({}, Date.now(), { | ||
userId: 'my-user', | ||
}); | ||
// mess with casing | ||
@@ -69,2 +94,6 @@ incomingRequest.headers[typings_1.ContentfulHeader.Signature.toUpperCase()] = | ||
incomingRequest.headers[typings_1.ContentfulHeader.Timestamp]; | ||
incomingRequest.headers[typings_1.ContentfulHeader.SpaceId.toUpperCase()] = | ||
incomingRequest.headers[typings_1.ContentfulHeader.SpaceId]; | ||
incomingRequest.headers[typings_1.ContentfulUserIdHeader.toUpperCase()] = | ||
incomingRequest.headers[typings_1.ContentfulUserIdHeader]; | ||
// remove correctly cased ones | ||
@@ -74,6 +103,10 @@ delete incomingRequest.headers[typings_1.ContentfulHeader.Signature]; | ||
delete incomingRequest.headers[typings_1.ContentfulHeader.Timestamp]; | ||
delete incomingRequest.headers[typings_1.ContentfulHeader.SpaceId]; | ||
delete incomingRequest.headers[typings_1.ContentfulUserIdHeader]; | ||
assert(verify_request_1.verifyRequest(VALID_SECRET, incomingRequest)); | ||
}); | ||
it('verifies correctly with keys with whitespace', () => { | ||
const incomingRequest = makeIncomingRequest({}); | ||
const incomingRequest = makeIncomingRequest({}, Date.now(), { | ||
appId: 'my-app', | ||
}); | ||
// mess with spacing | ||
@@ -86,2 +119,6 @@ incomingRequest.headers[`${typings_1.ContentfulHeader.Signature} `] = | ||
incomingRequest.headers[typings_1.ContentfulHeader.Timestamp]; | ||
incomingRequest.headers[` ${typings_1.ContentfulHeader.SpaceId} `] = | ||
incomingRequest.headers[typings_1.ContentfulHeader.SpaceId]; | ||
incomingRequest.headers[` ${typings_1.ContentfulAppIdHeader} `] = | ||
incomingRequest.headers[typings_1.ContentfulAppIdHeader]; | ||
// remove correctly spaced ones | ||
@@ -91,2 +128,4 @@ delete incomingRequest.headers[typings_1.ContentfulHeader.Signature]; | ||
delete incomingRequest.headers[typings_1.ContentfulHeader.Timestamp]; | ||
delete incomingRequest.headers[typings_1.ContentfulAppIdHeader]; | ||
delete incomingRequest.headers[typings_1.ContentfulHeader.SpaceId]; | ||
assert(verify_request_1.verifyRequest(VALID_SECRET, incomingRequest)); | ||
@@ -93,0 +132,0 @@ }); |
{ | ||
"name": "@contentful/node-apps-toolkit", | ||
"version": "1.3.2", | ||
"version": "1.4.0", | ||
"description": "A collection of helpers and utilities for creating NodeJS Contentful Apps", | ||
@@ -23,8 +23,9 @@ "main": "lib/index.js", | ||
"dependencies": { | ||
"@types/debug": "^4.1.5", | ||
"debug": "^4.2.0", | ||
"@types/debug": "^4.1.5", | ||
"got": "^11.7.0", | ||
"jsonwebtoken": "^8.5.1", | ||
"node-cache": "^5.1.2", | ||
"runtypes": "^5.0.1" | ||
"runtypes": "^5.0.1", | ||
"ts-xor": "^1.0.8" | ||
}, | ||
@@ -31,0 +32,0 @@ "devDependencies": { |
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
93139
1129
7
+ Addedts-xor@^1.0.8
+ Addedts-xor@1.3.0(transitive)