@cedarjs/api 7.0.0-canary.3111 → 7.0.0-canary.3113
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/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +4 -3
- package/dist/signedTokens/index.d.ts +2 -0
- package/dist/signedTokens/index.d.ts.map +1 -0
- package/dist/signedTokens/index.js +1 -0
- package/dist/signedTokens/signedTokens.d.ts +99 -0
- package/dist/signedTokens/signedTokens.d.ts.map +1 -0
- package/dist/signedTokens/signedTokens.js +100 -0
- package/package.json +2 -2
package/dist/index.d.ts
CHANGED
|
@@ -6,6 +6,7 @@ export * from './types.js';
|
|
|
6
6
|
export * from './transforms.js';
|
|
7
7
|
export * from './cors.js';
|
|
8
8
|
export * from './event.js';
|
|
9
|
+
export * from './signedTokens/index.js';
|
|
9
10
|
export declare const prismaVersion: string | undefined;
|
|
10
11
|
/** @deprecated - use `cedarVersion` instead */
|
|
11
12
|
export declare const redwoodVersion: string;
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,8BAA8B,CAAA;AAC5C,cAAc,yBAAyB,CAAA;AACvC,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAA;AAC/B,cAAc,aAAa,CAAA;AAC3B,cAAc,8BAA8B,CAAA;AAC5C,cAAc,yBAAyB,CAAA;AACvC,cAAc,YAAY,CAAA;AAC1B,cAAc,iBAAiB,CAAA;AAC/B,cAAc,WAAW,CAAA;AACzB,cAAc,YAAY,CAAA;AAC1B,cAAc,yBAAyB,CAAA;AAUvC,eAAO,MAAM,aAAa,EAAE,MAAM,GAAG,SAAqC,CAAA;AAC1E,+CAA+C;AAC/C,eAAO,MAAM,cAAc,QAAwB,CAAA;AACnD,+CAA+C;AAC/C,eAAO,MAAM,cAAc,QAAwB,CAAA;AACnD,eAAO,MAAM,YAAY,QAAwB,CAAA"}
|
package/dist/index.js
CHANGED
|
@@ -6,10 +6,11 @@ export * from "./types.js";
|
|
|
6
6
|
export * from "./transforms.js";
|
|
7
7
|
export * from "./cors.js";
|
|
8
8
|
export * from "./event.js";
|
|
9
|
+
export * from "./signedTokens/index.js";
|
|
9
10
|
const prismaVersion = "7.10.0";
|
|
10
|
-
const redwoodVersion = "7.0.0-canary.
|
|
11
|
-
const cedarjsVersion = "7.0.0-canary.
|
|
12
|
-
const cedarVersion = "7.0.0-canary.
|
|
11
|
+
const redwoodVersion = "7.0.0-canary.3113";
|
|
12
|
+
const cedarjsVersion = "7.0.0-canary.3113";
|
|
13
|
+
const cedarVersion = "7.0.0-canary.3113";
|
|
13
14
|
export {
|
|
14
15
|
cedarVersion,
|
|
15
16
|
cedarjsVersion,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/signedTokens/index.ts"],"names":[],"mappings":"AAAA,cAAc,mBAAmB,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "./signedTokens.js";
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
import type { SignOptions } from 'jsonwebtoken';
|
|
2
|
+
/**
|
|
3
|
+
* Name of the environment variable that holds the default secret used to
|
|
4
|
+
* sign and verify tokens when no `secret` option is passed.
|
|
5
|
+
*/
|
|
6
|
+
export declare const SIGNED_TOKEN_SECRET_ENV_VAR = "SIGNED_TOKEN_SECRET";
|
|
7
|
+
/**
|
|
8
|
+
* Machine-readable reason a `SignedTokenError` was thrown. Use it to tell an
|
|
9
|
+
* expired link apart from a tampered one without matching on the message.
|
|
10
|
+
*/
|
|
11
|
+
export type SignedTokenErrorCode = 'MISSING_SECRET' | 'MISSING_PURPOSE' | 'MISSING_TOKEN' | 'SIGN_FAILED' | 'INVALID' | 'EXPIRED' | 'PURPOSE_MISMATCH';
|
|
12
|
+
/**
|
|
13
|
+
* Thrown by `createSignedToken` and `verifySignedToken` whenever a token
|
|
14
|
+
* cannot be created or cannot be trusted. Verification never returns `null`
|
|
15
|
+
* or `false` for a bad token; it always throws so a missing check cannot
|
|
16
|
+
* silently pass.
|
|
17
|
+
*/
|
|
18
|
+
export declare class SignedTokenError extends Error {
|
|
19
|
+
code: SignedTokenErrorCode;
|
|
20
|
+
constructor(code: SignedTokenErrorCode, message: string, cause?: unknown);
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* How long a token stays valid. A number is a count of seconds. A string is
|
|
24
|
+
* a duration such as `'10m'`, `'2h'`, or `'7 days'`.
|
|
25
|
+
*/
|
|
26
|
+
export type SignedTokenExpiresIn = NonNullable<SignOptions['expiresIn']>;
|
|
27
|
+
export interface CreateSignedTokenOptions<TPayload extends object> {
|
|
28
|
+
/**
|
|
29
|
+
* The claims to carry inside the token. Serialized as JSON, so only
|
|
30
|
+
* JSON-compatible values survive the round trip (a `Date` comes back as a
|
|
31
|
+
* string).
|
|
32
|
+
*/
|
|
33
|
+
payload: TPayload;
|
|
34
|
+
/**
|
|
35
|
+
* What the token is for, such as `'google-oauth-state'` or
|
|
36
|
+
* `'email-confirmation'`. A token only verifies when the same purpose is
|
|
37
|
+
* passed to `verifySignedToken`, so a token minted for one flow cannot be
|
|
38
|
+
* replayed in another.
|
|
39
|
+
*/
|
|
40
|
+
purpose: string;
|
|
41
|
+
/** How long the token stays valid. See `SignedTokenExpiresIn`. */
|
|
42
|
+
expiresIn: SignedTokenExpiresIn;
|
|
43
|
+
/**
|
|
44
|
+
* The secret to sign with. Defaults to the `SIGNED_TOKEN_SECRET`
|
|
45
|
+
* environment variable. There is no built-in fallback: when neither is set
|
|
46
|
+
* a `SignedTokenError` with code `MISSING_SECRET` is thrown.
|
|
47
|
+
*/
|
|
48
|
+
secret?: string;
|
|
49
|
+
}
|
|
50
|
+
export interface VerifySignedTokenOptions {
|
|
51
|
+
/** The purpose the token must have been created with. */
|
|
52
|
+
purpose: string;
|
|
53
|
+
/**
|
|
54
|
+
* The secret to verify with. Defaults to the `SIGNED_TOKEN_SECRET`
|
|
55
|
+
* environment variable. There is no built-in fallback: when neither is set
|
|
56
|
+
* a `SignedTokenError` with code `MISSING_SECRET` is thrown.
|
|
57
|
+
*/
|
|
58
|
+
secret?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Creates a compact, URL-safe token that carries `payload`, is bound to
|
|
62
|
+
* `purpose`, expires after `expiresIn`, and is signed so it can only have
|
|
63
|
+
* come from this application.
|
|
64
|
+
*
|
|
65
|
+
* Use it wherever a value has to leave the server and come back untouched:
|
|
66
|
+
* OAuth `state`, email confirmation and password-set links, unsubscribe
|
|
67
|
+
* links, or short-lived capability tokens.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
*
|
|
71
|
+
* const state = createSignedToken({
|
|
72
|
+
* payload: { organizationId, userId },
|
|
73
|
+
* purpose: 'google-oauth-state',
|
|
74
|
+
* expiresIn: '10m',
|
|
75
|
+
* })
|
|
76
|
+
*/
|
|
77
|
+
export declare function createSignedToken<TPayload extends object>({ payload, purpose, expiresIn, secret, }: CreateSignedTokenOptions<TPayload>): string;
|
|
78
|
+
/**
|
|
79
|
+
* Verifies a token created by `createSignedToken` and returns its payload.
|
|
80
|
+
*
|
|
81
|
+
* Throws a `SignedTokenError` when the token is missing, was signed with a
|
|
82
|
+
* different secret, has been tampered with, has expired, or was created for
|
|
83
|
+
* a different `purpose`. The error's `code` says which.
|
|
84
|
+
*
|
|
85
|
+
* The token argument accepts `null` and `undefined` on purpose so a value
|
|
86
|
+
* read straight from a query string or a header can be passed in as-is. A
|
|
87
|
+
* missing token is a verification failure, never a skipped check.
|
|
88
|
+
*
|
|
89
|
+
* @example
|
|
90
|
+
*
|
|
91
|
+
* const { organizationId, userId } = verifySignedToken<{
|
|
92
|
+
* organizationId: string
|
|
93
|
+
* userId: string
|
|
94
|
+
* }>(event.queryStringParameters?.state, {
|
|
95
|
+
* purpose: 'google-oauth-state',
|
|
96
|
+
* })
|
|
97
|
+
*/
|
|
98
|
+
export declare function verifySignedToken<TPayload extends object = Record<string, unknown>>(token: string | null | undefined, { purpose, secret }: VerifySignedTokenOptions): TPayload;
|
|
99
|
+
//# sourceMappingURL=signedTokens.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signedTokens.d.ts","sourceRoot":"","sources":["../../src/signedTokens/signedTokens.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,cAAc,CAAA;AAE/C;;;GAGG;AACH,eAAO,MAAM,2BAA2B,wBAAwB,CAAA;AAEhE;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAC5B,gBAAgB,GAChB,iBAAiB,GACjB,eAAe,GACf,aAAa,GACb,SAAS,GACT,SAAS,GACT,kBAAkB,CAAA;AAEtB;;;;;GAKG;AACH,qBAAa,gBAAiB,SAAQ,KAAK;IACzC,IAAI,EAAE,oBAAoB,CAAA;gBAEd,IAAI,EAAE,oBAAoB,EAAE,OAAO,EAAE,MAAM,EAAE,KAAK,CAAC,EAAE,OAAO;CAKzE;AAED;;;GAGG;AACH,MAAM,MAAM,oBAAoB,GAAG,WAAW,CAAC,WAAW,CAAC,WAAW,CAAC,CAAC,CAAA;AAExE,MAAM,WAAW,wBAAwB,CAAC,QAAQ,SAAS,MAAM;IAC/D;;;;OAIG;IACH,OAAO,EAAE,QAAQ,CAAA;IACjB;;;;;OAKG;IACH,OAAO,EAAE,MAAM,CAAA;IACf,kEAAkE;IAClE,SAAS,EAAE,oBAAoB,CAAA;IAC/B;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAED,MAAM,WAAW,wBAAwB;IACvC,yDAAyD;IACzD,OAAO,EAAE,MAAM,CAAA;IACf;;;;OAIG;IACH,MAAM,CAAC,EAAE,MAAM,CAAA;CAChB;AAiED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAgB,iBAAiB,CAAC,QAAQ,SAAS,MAAM,EAAE,EACzD,OAAO,EACP,OAAO,EACP,SAAS,EACT,MAAM,GACP,EAAE,wBAAwB,CAAC,QAAQ,CAAC,GAAG,MAAM,CAkB7C;AAED;;;;;;;;;;;;;;;;;;;GAmBG;AACH,wBAAgB,iBAAiB,CAC/B,QAAQ,SAAS,MAAM,GAAG,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,EAEjD,KAAK,EAAE,MAAM,GAAG,IAAI,GAAG,SAAS,EAChC,EAAE,OAAO,EAAE,MAAM,EAAE,EAAE,wBAAwB,GAC5C,QAAQ,CA2CV"}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import jwt from "jsonwebtoken";
|
|
2
|
+
const SIGNED_TOKEN_SECRET_ENV_VAR = "SIGNED_TOKEN_SECRET";
|
|
3
|
+
class SignedTokenError extends Error {
|
|
4
|
+
code;
|
|
5
|
+
constructor(code, message, cause) {
|
|
6
|
+
super(message, cause === void 0 ? void 0 : { cause });
|
|
7
|
+
this.name = "SignedTokenError";
|
|
8
|
+
this.code = code;
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const ALGORITHM = "HS256";
|
|
12
|
+
function resolveSecret(secret, action) {
|
|
13
|
+
const resolved = secret ?? process.env[SIGNED_TOKEN_SECRET_ENV_VAR];
|
|
14
|
+
if (typeof resolved === "string" && resolved.length > 0) {
|
|
15
|
+
return resolved;
|
|
16
|
+
}
|
|
17
|
+
throw new SignedTokenError(
|
|
18
|
+
"MISSING_SECRET",
|
|
19
|
+
`Cannot ${action} a signed token because no secret is configured. Set the ${SIGNED_TOKEN_SECRET_ENV_VAR} environment variable (generate a value with \`yarn cedar generate secret\`), or pass the \`secret\` option explicitly.`
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
function assertPurpose(purpose) {
|
|
23
|
+
if (typeof purpose === "string" && purpose.length > 0) {
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
throw new SignedTokenError(
|
|
27
|
+
"MISSING_PURPOSE",
|
|
28
|
+
"A signed token needs a non-empty `purpose` string, such as 'google-oauth-state' or 'email-confirmation'. The purpose is what stops a token created for one flow from verifying in another."
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
function isSignedTokenClaims(claims) {
|
|
32
|
+
if (typeof claims !== "object" || claims === null) {
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
if (!("payload" in claims) || !("purpose" in claims) || !("exp" in claims)) {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
return typeof claims.payload === "object" && claims.payload !== null && typeof claims.purpose === "string" && typeof claims.exp === "number";
|
|
39
|
+
}
|
|
40
|
+
function createSignedToken({
|
|
41
|
+
payload,
|
|
42
|
+
purpose,
|
|
43
|
+
expiresIn,
|
|
44
|
+
secret
|
|
45
|
+
}) {
|
|
46
|
+
assertPurpose(purpose);
|
|
47
|
+
const resolvedSecret = resolveSecret(secret, "create");
|
|
48
|
+
try {
|
|
49
|
+
return jwt.sign({ payload, purpose }, resolvedSecret, {
|
|
50
|
+
algorithm: ALGORITHM,
|
|
51
|
+
expiresIn
|
|
52
|
+
});
|
|
53
|
+
} catch (e) {
|
|
54
|
+
const message = e instanceof Error ? e.message : String(e);
|
|
55
|
+
throw new SignedTokenError(
|
|
56
|
+
"SIGN_FAILED",
|
|
57
|
+
`Could not create signed token: ${message}`,
|
|
58
|
+
e
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function verifySignedToken(token, { purpose, secret }) {
|
|
63
|
+
assertPurpose(purpose);
|
|
64
|
+
const resolvedSecret = resolveSecret(secret, "verify");
|
|
65
|
+
if (typeof token !== "string" || token.length === 0) {
|
|
66
|
+
throw new SignedTokenError("MISSING_TOKEN", "No signed token was provided.");
|
|
67
|
+
}
|
|
68
|
+
let claims;
|
|
69
|
+
try {
|
|
70
|
+
claims = jwt.verify(token, resolvedSecret, { algorithms: [ALGORITHM] });
|
|
71
|
+
} catch (e) {
|
|
72
|
+
if (e instanceof jwt.TokenExpiredError) {
|
|
73
|
+
throw new SignedTokenError("EXPIRED", "The signed token has expired.", e);
|
|
74
|
+
}
|
|
75
|
+
throw new SignedTokenError(
|
|
76
|
+
"INVALID",
|
|
77
|
+
"The signed token is invalid or was not created by this application.",
|
|
78
|
+
e
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
if (!isSignedTokenClaims(claims)) {
|
|
82
|
+
throw new SignedTokenError(
|
|
83
|
+
"INVALID",
|
|
84
|
+
"The signed token is invalid or was not created by this application."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (claims.purpose !== purpose) {
|
|
88
|
+
throw new SignedTokenError(
|
|
89
|
+
"PURPOSE_MISMATCH",
|
|
90
|
+
`The signed token was created for purpose '${claims.purpose}' but was verified with purpose '${purpose}'.`
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
return claims.payload;
|
|
94
|
+
}
|
|
95
|
+
export {
|
|
96
|
+
SIGNED_TOKEN_SECRET_ENV_VAR,
|
|
97
|
+
SignedTokenError,
|
|
98
|
+
createSignedToken,
|
|
99
|
+
verifySignedToken
|
|
100
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cedarjs/api",
|
|
3
|
-
"version": "7.0.0-canary.
|
|
3
|
+
"version": "7.0.0-canary.3113",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/cedarjs/cedar.git",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"title-case": "3.0.3"
|
|
85
85
|
},
|
|
86
86
|
"devDependencies": {
|
|
87
|
-
"@cedarjs/framework-tools": "7.0.0-canary.
|
|
87
|
+
"@cedarjs/framework-tools": "7.0.0-canary.3113",
|
|
88
88
|
"@types/aws-lambda": "8.10.163",
|
|
89
89
|
"@types/jsonwebtoken": "9.0.10",
|
|
90
90
|
"@types/memjs": "1",
|