@codelitdev/oauth-server-kit 0.1.0-alpha.0 → 0.1.0-alpha.2
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/README.md +4 -26
- package/dist/better-auth.js +8 -17
- package/dist/better-auth.mjs +9 -18
- package/dist/{chunk-I4E63NIC.mjs → chunk-FWCSY2DS.mjs} +13 -1
- package/dist/{chunk-RJ4ABQXG.mjs → chunk-JENFPLCB.mjs} +3 -5
- package/dist/express.d.mts +5 -1
- package/dist/express.d.ts +5 -1
- package/dist/express.js +97 -35
- package/dist/express.mjs +85 -33
- package/dist/index.js +2 -4
- package/dist/index.mjs +2 -2
- package/dist/mcp.d.mts +4 -0
- package/dist/mcp.d.ts +4 -0
- package/dist/mcp.js +20 -36
- package/dist/mcp.mjs +21 -37
- package/package.json +96 -96
- package/LICENSE +0 -21
package/README.md
CHANGED
|
@@ -8,17 +8,6 @@ The library authenticates people and OAuth clients. Each product continues to
|
|
|
8
8
|
own its users-to-actor mapping, organizations, teams, memberships, roles,
|
|
9
9
|
permissions, API keys, and credential precedence.
|
|
10
10
|
|
|
11
|
-
## Install
|
|
12
|
-
|
|
13
|
-
The package is currently released through npm's `alpha` channel:
|
|
14
|
-
|
|
15
|
-
```sh
|
|
16
|
-
pnpm add @codelitdev/oauth-server-kit@alpha
|
|
17
|
-
```
|
|
18
|
-
|
|
19
|
-
It has a pre-1.0 public API. Pin an explicit version for production consumers
|
|
20
|
-
and upgrade deliberately.
|
|
21
|
-
|
|
22
11
|
## Supported surfaces
|
|
23
12
|
|
|
24
13
|
- Web apps use product-local Better Auth sessions.
|
|
@@ -76,23 +65,12 @@ product requirements and public contract.
|
|
|
76
65
|
## Development gates
|
|
77
66
|
|
|
78
67
|
```bash
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
68
|
+
bun run check-types
|
|
69
|
+
bun test
|
|
70
|
+
bun run test:coverage
|
|
71
|
+
bun run build
|
|
83
72
|
```
|
|
84
73
|
|
|
85
74
|
The coverage release gate is 90% statements, lines, and functions and 80%
|
|
86
75
|
branches. The suite includes a real Better Auth OAuth Provider flow with a
|
|
87
76
|
public client, S256 PKCE, refresh, JWKS verification, and MCP discovery.
|
|
88
|
-
|
|
89
|
-
## Releasing an alpha
|
|
90
|
-
|
|
91
|
-
After updating the package version, run:
|
|
92
|
-
|
|
93
|
-
```sh
|
|
94
|
-
npm run release:alpha -- --otp=<npm-otp>
|
|
95
|
-
```
|
|
96
|
-
|
|
97
|
-
The command validates formatting, linting, types, tests, coverage, and the
|
|
98
|
-
build before publishing to npm with the `alpha` dist-tag.
|
package/dist/better-auth.js
CHANGED
|
@@ -57,9 +57,7 @@ async function resolveBetterAuthSession(options, headers) {
|
|
|
57
57
|
}
|
|
58
58
|
}
|
|
59
59
|
function uniqueRequired(values, field) {
|
|
60
|
-
const result = [
|
|
61
|
-
...new Set(values.map((value) => value.trim()).filter(Boolean))
|
|
62
|
-
];
|
|
60
|
+
const result = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
63
61
|
if (result.length === 0) {
|
|
64
62
|
throw new Error(`${field} must contain at least one value`);
|
|
65
63
|
}
|
|
@@ -68,9 +66,7 @@ function uniqueRequired(values, field) {
|
|
|
68
66
|
function assertPageLocation(value, field) {
|
|
69
67
|
if (value.startsWith("/") && !value.startsWith("//")) {
|
|
70
68
|
if (/\s/.test(value)) {
|
|
71
|
-
throw new Error(
|
|
72
|
-
`${field} must be an absolute URL or absolute path`
|
|
73
|
-
);
|
|
69
|
+
throw new Error(`${field} must be an absolute URL or absolute path`);
|
|
74
70
|
}
|
|
75
71
|
return;
|
|
76
72
|
}
|
|
@@ -89,14 +85,10 @@ function assertPageLocation(value, field) {
|
|
|
89
85
|
}
|
|
90
86
|
function assertScopeSubset(values, scopes, field) {
|
|
91
87
|
if (!values) return void 0;
|
|
92
|
-
const normalized = [
|
|
93
|
-
...new Set(values.map((value) => value.trim()).filter(Boolean))
|
|
94
|
-
];
|
|
88
|
+
const normalized = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
95
89
|
const invalid = normalized.filter((scope) => !scopes.includes(scope));
|
|
96
90
|
if (invalid.length > 0) {
|
|
97
|
-
throw new Error(
|
|
98
|
-
`${field} contains unsupported scopes: ${invalid.join(", ")}`
|
|
99
|
-
);
|
|
91
|
+
throw new Error(`${field} contains unsupported scopes: ${invalid.join(", ")}`);
|
|
100
92
|
}
|
|
101
93
|
return normalized;
|
|
102
94
|
}
|
|
@@ -105,10 +97,7 @@ function createOAuthProviderOptions(input) {
|
|
|
105
97
|
assertPageLocation(input.loginPage, "loginPage");
|
|
106
98
|
assertPageLocation(input.consentPage, "consentPage");
|
|
107
99
|
const scopes = uniqueRequired(input.scopes, "scopes");
|
|
108
|
-
const validAudiences = uniqueRequired(
|
|
109
|
-
input.validAudiences,
|
|
110
|
-
"validAudiences"
|
|
111
|
-
);
|
|
100
|
+
const validAudiences = uniqueRequired(input.validAudiences, "validAudiences");
|
|
112
101
|
if (input.allowUnauthenticatedDynamicClientRegistration && !input.allowDynamicClientRegistration) {
|
|
113
102
|
throw new Error(
|
|
114
103
|
"unauthenticated dynamic client registration requires dynamic client registration"
|
|
@@ -128,7 +117,9 @@ function createOAuthProviderOptions(input) {
|
|
|
128
117
|
loginPage: input.loginPage,
|
|
129
118
|
consentPage: input.consentPage,
|
|
130
119
|
scopes,
|
|
131
|
-
validAudiences,
|
|
120
|
+
resources: validAudiences,
|
|
121
|
+
clientRegistrationDefaultResources: validAudiences,
|
|
122
|
+
clientRegistrationAllowedResources: validAudiences,
|
|
132
123
|
allowDynamicClientRegistration: (_a = input.allowDynamicClientRegistration) != null ? _a : false,
|
|
133
124
|
allowUnauthenticatedClientRegistration: (_b = input.allowUnauthenticatedDynamicClientRegistration) != null ? _b : false
|
|
134
125
|
}, clientRegistrationDefaultScopes ? { clientRegistrationDefaultScopes } : {}), clientRegistrationAllowedScopes ? { clientRegistrationAllowedScopes } : {});
|
package/dist/better-auth.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __spreadValues } from './chunk-
|
|
1
|
+
import { __spreadValues } from './chunk-FWCSY2DS.mjs';
|
|
2
2
|
|
|
3
3
|
// src/better-auth.ts
|
|
4
4
|
function normalizeIssuer(value) {
|
|
@@ -40,9 +40,7 @@ async function resolveBetterAuthSession(options, headers) {
|
|
|
40
40
|
}
|
|
41
41
|
}
|
|
42
42
|
function uniqueRequired(values, field) {
|
|
43
|
-
const result = [
|
|
44
|
-
...new Set(values.map((value) => value.trim()).filter(Boolean))
|
|
45
|
-
];
|
|
43
|
+
const result = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
46
44
|
if (result.length === 0) {
|
|
47
45
|
throw new Error(`${field} must contain at least one value`);
|
|
48
46
|
}
|
|
@@ -51,9 +49,7 @@ function uniqueRequired(values, field) {
|
|
|
51
49
|
function assertPageLocation(value, field) {
|
|
52
50
|
if (value.startsWith("/") && !value.startsWith("//")) {
|
|
53
51
|
if (/\s/.test(value)) {
|
|
54
|
-
throw new Error(
|
|
55
|
-
`${field} must be an absolute URL or absolute path`
|
|
56
|
-
);
|
|
52
|
+
throw new Error(`${field} must be an absolute URL or absolute path`);
|
|
57
53
|
}
|
|
58
54
|
return;
|
|
59
55
|
}
|
|
@@ -72,14 +68,10 @@ function assertPageLocation(value, field) {
|
|
|
72
68
|
}
|
|
73
69
|
function assertScopeSubset(values, scopes, field) {
|
|
74
70
|
if (!values) return void 0;
|
|
75
|
-
const normalized = [
|
|
76
|
-
...new Set(values.map((value) => value.trim()).filter(Boolean))
|
|
77
|
-
];
|
|
71
|
+
const normalized = [...new Set(values.map((value) => value.trim()).filter(Boolean))];
|
|
78
72
|
const invalid = normalized.filter((scope) => !scopes.includes(scope));
|
|
79
73
|
if (invalid.length > 0) {
|
|
80
|
-
throw new Error(
|
|
81
|
-
`${field} contains unsupported scopes: ${invalid.join(", ")}`
|
|
82
|
-
);
|
|
74
|
+
throw new Error(`${field} contains unsupported scopes: ${invalid.join(", ")}`);
|
|
83
75
|
}
|
|
84
76
|
return normalized;
|
|
85
77
|
}
|
|
@@ -88,10 +80,7 @@ function createOAuthProviderOptions(input) {
|
|
|
88
80
|
assertPageLocation(input.loginPage, "loginPage");
|
|
89
81
|
assertPageLocation(input.consentPage, "consentPage");
|
|
90
82
|
const scopes = uniqueRequired(input.scopes, "scopes");
|
|
91
|
-
const validAudiences = uniqueRequired(
|
|
92
|
-
input.validAudiences,
|
|
93
|
-
"validAudiences"
|
|
94
|
-
);
|
|
83
|
+
const validAudiences = uniqueRequired(input.validAudiences, "validAudiences");
|
|
95
84
|
if (input.allowUnauthenticatedDynamicClientRegistration && !input.allowDynamicClientRegistration) {
|
|
96
85
|
throw new Error(
|
|
97
86
|
"unauthenticated dynamic client registration requires dynamic client registration"
|
|
@@ -111,7 +100,9 @@ function createOAuthProviderOptions(input) {
|
|
|
111
100
|
loginPage: input.loginPage,
|
|
112
101
|
consentPage: input.consentPage,
|
|
113
102
|
scopes,
|
|
114
|
-
validAudiences,
|
|
103
|
+
resources: validAudiences,
|
|
104
|
+
clientRegistrationDefaultResources: validAudiences,
|
|
105
|
+
clientRegistrationAllowedResources: validAudiences,
|
|
115
106
|
allowDynamicClientRegistration: (_a = input.allowDynamicClientRegistration) != null ? _a : false,
|
|
116
107
|
allowUnauthenticatedClientRegistration: (_b = input.allowUnauthenticatedDynamicClientRegistration) != null ? _b : false
|
|
117
108
|
}, clientRegistrationDefaultScopes ? { clientRegistrationDefaultScopes } : {}), clientRegistrationAllowedScopes ? { clientRegistrationAllowedScopes } : {});
|
|
@@ -17,5 +17,17 @@ var __spreadValues = (a, b) => {
|
|
|
17
17
|
return a;
|
|
18
18
|
};
|
|
19
19
|
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
20
|
+
var __objRest = (source, exclude) => {
|
|
21
|
+
var target = {};
|
|
22
|
+
for (var prop in source)
|
|
23
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
24
|
+
target[prop] = source[prop];
|
|
25
|
+
if (source != null && __getOwnPropSymbols)
|
|
26
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
27
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
28
|
+
target[prop] = source[prop];
|
|
29
|
+
}
|
|
30
|
+
return target;
|
|
31
|
+
};
|
|
20
32
|
|
|
21
|
-
export { __spreadProps, __spreadValues };
|
|
33
|
+
export { __objRest, __spreadProps, __spreadValues };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __spreadProps, __spreadValues } from './chunk-
|
|
1
|
+
import { __spreadProps, __spreadValues } from './chunk-FWCSY2DS.mjs';
|
|
2
2
|
|
|
3
3
|
// src/verify-access-token.ts
|
|
4
4
|
function requireAbsoluteUrl(value, field) {
|
|
@@ -44,16 +44,14 @@ async function verifyOAuthAccessToken(options, token) {
|
|
|
44
44
|
const audiences = normalizedUniqueStrings(options.audiences);
|
|
45
45
|
if (!token.trim()) return { status: "invalid_token" };
|
|
46
46
|
try {
|
|
47
|
-
const claims = await options.oauthResourceClient.getActions().
|
|
47
|
+
const claims = await options.oauthResourceClient.getActions().verifyBearerToken(token, {
|
|
48
48
|
verifyOptions: { issuer, audience: audiences },
|
|
49
49
|
resourceMetadataMappings: (_a = options.resourceMetadataMappings) != null ? _a : {}
|
|
50
50
|
});
|
|
51
51
|
const subject = optionalString(claims.sub);
|
|
52
52
|
const claimIssuer = optionalString(claims.iss);
|
|
53
53
|
const clientId = (_b = optionalString(claims.azp)) != null ? _b : optionalString(claims.client_id);
|
|
54
|
-
const tokenAudiences = normalizedUniqueStrings(
|
|
55
|
-
claimStrings(claims.aud)
|
|
56
|
-
);
|
|
54
|
+
const tokenAudiences = normalizedUniqueStrings(claimStrings(claims.aud));
|
|
57
55
|
const validatedAudiences = tokenAudiences.filter(
|
|
58
56
|
(audience) => audiences.includes(audience)
|
|
59
57
|
);
|
package/dist/express.d.mts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RequestHandler, Router } from 'express';
|
|
2
|
-
import { a as AuthenticatedIdentity } from './types-DJxCmhYu.mjs';
|
|
2
|
+
import { a as AuthenticatedIdentity, A as AuthenticationResult } from './types-DJxCmhYu.mjs';
|
|
3
3
|
import { VerifyOAuthAccessTokenOptions } from './index.mjs';
|
|
4
4
|
import '@better-auth/oauth-provider/resource-client';
|
|
5
5
|
|
|
@@ -12,6 +12,7 @@ declare global {
|
|
|
12
12
|
}
|
|
13
13
|
interface CreateOAuthBearerMiddlewareOptions extends VerifyOAuthAccessTokenOptions {
|
|
14
14
|
resourceMetadataUrl?: string;
|
|
15
|
+
verifyAccessToken?: (options: VerifyOAuthAccessTokenOptions, token: string) => Promise<AuthenticationResult>;
|
|
15
16
|
}
|
|
16
17
|
declare function createOAuthBearerMiddleware(options: CreateOAuthBearerMiddlewareOptions): RequestHandler;
|
|
17
18
|
|
|
@@ -30,6 +31,9 @@ interface CreateOAuthPagesRouterOptions {
|
|
|
30
31
|
defaultRedirectUrl: string;
|
|
31
32
|
loginMethods: readonly HostedLoginMethod[];
|
|
32
33
|
legacyHostOnlySessionCookieNames?: readonly string[];
|
|
34
|
+
logoUrl?: string;
|
|
35
|
+
faviconUrl?: string;
|
|
36
|
+
primaryColor?: string;
|
|
33
37
|
}
|
|
34
38
|
declare function createOAuthPagesRouter(rawOptions: CreateOAuthPagesRouterOptions): Router;
|
|
35
39
|
|
package/dist/express.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { RequestHandler, Router } from 'express';
|
|
2
|
-
import { a as AuthenticatedIdentity } from './types-DJxCmhYu.js';
|
|
2
|
+
import { a as AuthenticatedIdentity, A as AuthenticationResult } from './types-DJxCmhYu.js';
|
|
3
3
|
import { VerifyOAuthAccessTokenOptions } from './index.js';
|
|
4
4
|
import '@better-auth/oauth-provider/resource-client';
|
|
5
5
|
|
|
@@ -12,6 +12,7 @@ declare global {
|
|
|
12
12
|
}
|
|
13
13
|
interface CreateOAuthBearerMiddlewareOptions extends VerifyOAuthAccessTokenOptions {
|
|
14
14
|
resourceMetadataUrl?: string;
|
|
15
|
+
verifyAccessToken?: (options: VerifyOAuthAccessTokenOptions, token: string) => Promise<AuthenticationResult>;
|
|
15
16
|
}
|
|
16
17
|
declare function createOAuthBearerMiddleware(options: CreateOAuthBearerMiddlewareOptions): RequestHandler;
|
|
17
18
|
|
|
@@ -30,6 +31,9 @@ interface CreateOAuthPagesRouterOptions {
|
|
|
30
31
|
defaultRedirectUrl: string;
|
|
31
32
|
loginMethods: readonly HostedLoginMethod[];
|
|
32
33
|
legacyHostOnlySessionCookieNames?: readonly string[];
|
|
34
|
+
logoUrl?: string;
|
|
35
|
+
faviconUrl?: string;
|
|
36
|
+
primaryColor?: string;
|
|
33
37
|
}
|
|
34
38
|
declare function createOAuthPagesRouter(rawOptions: CreateOAuthPagesRouterOptions): Router;
|
|
35
39
|
|
package/dist/express.js
CHANGED
|
@@ -21,6 +21,18 @@ var __spreadValues = (a, b) => {
|
|
|
21
21
|
return a;
|
|
22
22
|
};
|
|
23
23
|
var __spreadProps = (a, b) => __defProps(a, __getOwnPropDescs(b));
|
|
24
|
+
var __objRest = (source, exclude) => {
|
|
25
|
+
var target = {};
|
|
26
|
+
for (var prop in source)
|
|
27
|
+
if (__hasOwnProp.call(source, prop) && exclude.indexOf(prop) < 0)
|
|
28
|
+
target[prop] = source[prop];
|
|
29
|
+
if (source != null && __getOwnPropSymbols)
|
|
30
|
+
for (var prop of __getOwnPropSymbols(source)) {
|
|
31
|
+
if (exclude.indexOf(prop) < 0 && __propIsEnum.call(source, prop))
|
|
32
|
+
target[prop] = source[prop];
|
|
33
|
+
}
|
|
34
|
+
return target;
|
|
35
|
+
};
|
|
24
36
|
|
|
25
37
|
// src/verify-access-token.ts
|
|
26
38
|
function requireAbsoluteUrl(value, field) {
|
|
@@ -66,16 +78,14 @@ async function verifyOAuthAccessToken(options, token) {
|
|
|
66
78
|
const audiences = normalizedUniqueStrings(options.audiences);
|
|
67
79
|
if (!token.trim()) return { status: "invalid_token" };
|
|
68
80
|
try {
|
|
69
|
-
const claims = await options.oauthResourceClient.getActions().
|
|
81
|
+
const claims = await options.oauthResourceClient.getActions().verifyBearerToken(token, {
|
|
70
82
|
verifyOptions: { issuer, audience: audiences },
|
|
71
83
|
resourceMetadataMappings: (_a = options.resourceMetadataMappings) != null ? _a : {}
|
|
72
84
|
});
|
|
73
85
|
const subject = optionalString(claims.sub);
|
|
74
86
|
const claimIssuer = optionalString(claims.iss);
|
|
75
87
|
const clientId = (_b = optionalString(claims.azp)) != null ? _b : optionalString(claims.client_id);
|
|
76
|
-
const tokenAudiences = normalizedUniqueStrings(
|
|
77
|
-
claimStrings(claims.aud)
|
|
78
|
-
);
|
|
88
|
+
const tokenAudiences = normalizedUniqueStrings(claimStrings(claims.aud));
|
|
79
89
|
const validatedAudiences = tokenAudiences.filter(
|
|
80
90
|
(audience) => audiences.includes(audience)
|
|
81
91
|
);
|
|
@@ -142,7 +152,8 @@ function bearerToken(request) {
|
|
|
142
152
|
return ((_a = match == null ? void 0 : match[1]) == null ? void 0 : _a.trim()) || null;
|
|
143
153
|
}
|
|
144
154
|
function createOAuthBearerMiddleware(options) {
|
|
145
|
-
|
|
155
|
+
const _a = options, { verifyAccessToken = verifyOAuthAccessToken } = _a, verificationOptions = __objRest(_a, ["verifyAccessToken"]);
|
|
156
|
+
validateOAuthVerificationOptions(verificationOptions);
|
|
146
157
|
if (options.resourceMetadataUrl) {
|
|
147
158
|
let metadataUrl;
|
|
148
159
|
try {
|
|
@@ -167,13 +178,9 @@ function createOAuthBearerMiddleware(options) {
|
|
|
167
178
|
);
|
|
168
179
|
return;
|
|
169
180
|
}
|
|
170
|
-
const result = await
|
|
181
|
+
const result = await verifyAccessToken(verificationOptions, token);
|
|
171
182
|
if (result.status !== "authenticated") {
|
|
172
|
-
sendAuthenticationError(
|
|
173
|
-
response,
|
|
174
|
-
result,
|
|
175
|
-
options.resourceMetadataUrl
|
|
176
|
-
);
|
|
183
|
+
sendAuthenticationError(response, result, options.resourceMetadataUrl);
|
|
177
184
|
return;
|
|
178
185
|
}
|
|
179
186
|
request.auth = result.identity;
|
|
@@ -209,6 +216,41 @@ function absoluteOrigin(value, field) {
|
|
|
209
216
|
}
|
|
210
217
|
return url.origin;
|
|
211
218
|
}
|
|
219
|
+
var HEX_COLOR = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
220
|
+
var OKLCH_COLOR = /^oklch\([0-9.%\s/,+-]+\)$/i;
|
|
221
|
+
function brandingColor(value, field) {
|
|
222
|
+
if (value === void 0) return void 0;
|
|
223
|
+
const color = value.trim();
|
|
224
|
+
if (HEX_COLOR.test(color) || OKLCH_COLOR.test(color)) return color;
|
|
225
|
+
throw new Error(`${field} must be a hex or oklch() color`);
|
|
226
|
+
}
|
|
227
|
+
function brandingUrl(value, field) {
|
|
228
|
+
if (value === void 0) return void 0;
|
|
229
|
+
const raw = value.trim();
|
|
230
|
+
if (!raw) throw new Error(`${field} must be a URL`);
|
|
231
|
+
if (raw.startsWith("/") && !raw.startsWith("//")) {
|
|
232
|
+
if (/\s/.test(raw) || raw.includes("\\") || raw.includes("<")) {
|
|
233
|
+
throw new Error(`${field} must be a safe URL`);
|
|
234
|
+
}
|
|
235
|
+
return raw;
|
|
236
|
+
}
|
|
237
|
+
let url;
|
|
238
|
+
try {
|
|
239
|
+
url = new URL(raw);
|
|
240
|
+
} catch (e) {
|
|
241
|
+
throw new Error(`${field} must be an absolute URL`);
|
|
242
|
+
}
|
|
243
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
244
|
+
throw new Error(`${field} must use http or https`);
|
|
245
|
+
}
|
|
246
|
+
if (process.env.NODE_ENV === "production" && url.protocol !== "https:") {
|
|
247
|
+
throw new Error(`${field} must use https in production`);
|
|
248
|
+
}
|
|
249
|
+
if (url.username || url.password) {
|
|
250
|
+
throw new Error(`${field} must not contain credentials`);
|
|
251
|
+
}
|
|
252
|
+
return url.toString();
|
|
253
|
+
}
|
|
212
254
|
function normalizeOptions(options) {
|
|
213
255
|
var _a;
|
|
214
256
|
if (!options.appName.trim()) throw new Error("appName is required");
|
|
@@ -248,9 +290,7 @@ function normalizeOptions(options) {
|
|
|
248
290
|
for (const method of options.loginMethods) {
|
|
249
291
|
if (method.type === "social") {
|
|
250
292
|
if (!method.providerId.trim() || !method.label.trim()) {
|
|
251
|
-
throw new Error(
|
|
252
|
-
"social login methods require providerId and label"
|
|
253
|
-
);
|
|
293
|
+
throw new Error("social login methods require providerId and label");
|
|
254
294
|
}
|
|
255
295
|
}
|
|
256
296
|
}
|
|
@@ -263,19 +303,42 @@ function normalizeOptions(options) {
|
|
|
263
303
|
appName: options.appName.trim(),
|
|
264
304
|
authBasePath,
|
|
265
305
|
allowedRedirectOrigins,
|
|
266
|
-
defaultRedirectUrl: defaultRedirect.toString()
|
|
306
|
+
defaultRedirectUrl: defaultRedirect.toString(),
|
|
307
|
+
logoUrl: brandingUrl(options.logoUrl, "logoUrl"),
|
|
308
|
+
faviconUrl: brandingUrl(options.faviconUrl, "faviconUrl"),
|
|
309
|
+
primaryColor: brandingColor(options.primaryColor, "primaryColor")
|
|
267
310
|
});
|
|
268
311
|
}
|
|
269
312
|
var STYLES = `
|
|
270
|
-
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#
|
|
271
|
-
.card{width:100%;max-width:400px;padding:32px;border:1px solid #
|
|
272
|
-
h1{margin:0 0
|
|
273
|
-
form{margin:0}.field{margin-bottom:14px}label{display:block;margin-bottom:6px;font-size:12px}input{width:100%;padding:11px 13px;border:1px solid #
|
|
274
|
-
button{width:100%;margin-top:8px;padding:11px;border:1px solid #
|
|
275
|
-
|
|
313
|
+
html{--brand-primary:#8c7a6b;color-scheme:light}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#f4f4f5;color:#171717;font-family:system-ui,sans-serif}
|
|
314
|
+
.card{width:100%;max-width:400px;padding:32px;border:1px solid #e4e4e7;border-radius:14px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.04)}.logo{width:38px;height:38px;margin:0 auto 20px;display:grid;place-items:center;border-radius:50%;background:#171717;color:#fff;font-weight:700}.logo.has-image{background:transparent;border-radius:8px}.logo img{width:38px;height:38px;object-fit:contain;display:block}
|
|
315
|
+
h1{margin:0 0 24px;text-align:center;font-size:22px;font-weight:650}.sub{margin:-12px 0 24px;text-align:center;color:#71717a;font-size:13px;line-height:1.5}.error{display:none;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#fef2f2;color:#b42318;font-size:13px}
|
|
316
|
+
form{margin:0}.field{margin-bottom:14px}label{display:block;margin-bottom:6px;font-size:12px;color:#52525b}input{width:100%;padding:11px 13px;border:1px solid #d4d4d8;border-radius:8px;background:#fff;color:#171717;font-size:15px}input:focus{outline:2px solid var(--brand-primary);outline-offset:1px;border-color:transparent}
|
|
317
|
+
button{width:100%;margin-top:8px;padding:11px;border:1px solid #d4d4d8;border-radius:8px;background:#fafafa;color:#171717;font-size:14px;font-weight:550;cursor:pointer}button:focus-visible{outline:2px solid var(--brand-primary);outline-offset:2px}.primary{border-color:var(--brand-primary);background:var(--brand-primary);color:#fff}.secondary{color:#52525b;background:transparent}.social{display:inline-flex;align-items:center;justify-content:center;gap:10px;background:#fff;color:#1f1f1f;border-color:#dadce0;font-weight:500}.social:hover{background:#f7f8f8}.social svg{width:18px;height:18px;flex:0 0 18px}
|
|
318
|
+
.divider{display:flex;align-items:center;gap:10px;margin:18px 0;color:#a1a1aa;font-size:12px}.divider:before,.divider:after{content:"";flex:1;height:1px;background:#e4e4e7}
|
|
319
|
+
button:disabled{opacity:.55;cursor:not-allowed}.client{margin:20px 0;padding:14px;border:1px solid #e4e4e7;border-radius:9px;background:#fafafa}.client-name{font-weight:600;word-break:break-all}.scopes{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.scope{padding:4px 7px;border:1px solid #e4e4e7;border-radius:6px;color:#52525b;font-size:12px}.actions{display:flex;gap:8px}.actions button{margin:0}
|
|
276
320
|
`;
|
|
277
|
-
function page(title, body) {
|
|
278
|
-
|
|
321
|
+
function page(title, body, options) {
|
|
322
|
+
const favicon = options.faviconUrl ? `<link rel="icon" href="${escapeHtml(options.faviconUrl)}">` : "";
|
|
323
|
+
const brandColor = options.primaryColor ? `html{--brand-primary:${options.primaryColor}}` : "";
|
|
324
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title>${favicon}<style>${STYLES}${brandColor}</style></head><body><main class="card">${body}</main></body></html>`;
|
|
325
|
+
}
|
|
326
|
+
function brandMark(options) {
|
|
327
|
+
var _a;
|
|
328
|
+
if (options.logoUrl) {
|
|
329
|
+
return `<div class="logo has-image"><img src="${escapeHtml(options.logoUrl)}" alt="${escapeHtml(options.appName)}"></div>`;
|
|
330
|
+
}
|
|
331
|
+
return `<div class="logo">${escapeHtml((_a = options.appName[0]) != null ? _a : "?")}</div>`;
|
|
332
|
+
}
|
|
333
|
+
var SOCIAL_ICONS = {
|
|
334
|
+
google: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" aria-hidden="true"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>',
|
|
335
|
+
github: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82A7.7 7.7 0 0 1 8 4.84c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/></svg>',
|
|
336
|
+
microsoft: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23" aria-hidden="true"><path fill="#f35325" d="M1 1h10v10H1z"/><path fill="#81bc06" d="M12 1h10v10H12z"/><path fill="#05a6f0" d="M1 12h10v10H1z"/><path fill="#ffba08" d="M12 12h10v10H12z"/></svg>'
|
|
337
|
+
};
|
|
338
|
+
function socialButton(method) {
|
|
339
|
+
var _a;
|
|
340
|
+
const icon = (_a = SOCIAL_ICONS[method.providerId]) != null ? _a : "";
|
|
341
|
+
return `<button type="button" class="social" data-provider="${escapeHtml(method.providerId)}">${icon}<span>${escapeHtml(method.label)}</span></button>`;
|
|
279
342
|
}
|
|
280
343
|
function clearLegacyCookies(response, names) {
|
|
281
344
|
for (const name of names != null ? names : []) {
|
|
@@ -289,19 +352,17 @@ function clearLegacyCookies(response, names) {
|
|
|
289
352
|
}
|
|
290
353
|
}
|
|
291
354
|
function loginMarkup(options) {
|
|
292
|
-
var _a
|
|
355
|
+
var _a;
|
|
293
356
|
const emailMethod = options.loginMethods.find(
|
|
294
357
|
(method) => method.type === "email-otp"
|
|
295
358
|
);
|
|
296
359
|
const socialMethods = options.loginMethods.filter(
|
|
297
360
|
(method) => method.type === "social"
|
|
298
361
|
);
|
|
299
|
-
const email = emailMethod ? `<form id="email-form"><div class="field"><label for="email">Email</label><input id="email" type="email" required autocomplete="email"></div><button class="primary" type="submit">${escapeHtml((_a = emailMethod.label) != null ? _a : "Continue with email")}</button></form><form id="otp-form" hidden><div class="field"><label for="otp">Verification code</label><input id="otp" inputmode="numeric" required autocomplete="one-time-code"></div><button class="primary" type="submit">Sign in</button><button class="secondary" type="button" id="change-email">Use a different email</button></form>` : "";
|
|
300
|
-
const social = socialMethods.map(
|
|
301
|
-
(method) => `<button type="button" class="social" data-provider="${escapeHtml(method.providerId)}">${escapeHtml(method.label)}</button>`
|
|
302
|
-
).join("");
|
|
362
|
+
const email = emailMethod ? `<form id="email-form"><div class="field"><label for="email">Email address</label><input id="email" type="email" required autocomplete="email"></div><button class="primary" type="submit">${escapeHtml((_a = emailMethod.label) != null ? _a : "Continue with email")}</button></form><form id="otp-form" hidden><div class="field"><label for="otp">Verification code</label><input id="otp" inputmode="numeric" required autocomplete="one-time-code"></div><button class="primary" type="submit">Sign in</button><button class="secondary" type="button" id="change-email">Use a different email</button></form>` : "";
|
|
363
|
+
const social = socialMethods.map(socialButton).join("");
|
|
303
364
|
const divider = email && social ? '<div class="divider">or</div>' : "";
|
|
304
|
-
return
|
|
365
|
+
return `${brandMark(options)}<h1>Sign in to ${escapeHtml(options.appName)}</h1><div class="error" id="error" role="alert"></div>${email}${divider}${social}`;
|
|
305
366
|
}
|
|
306
367
|
function loginScript(options, oauth, redirect) {
|
|
307
368
|
const authBasePath = scriptLiteral(options.authBasePath);
|
|
@@ -310,7 +371,7 @@ function loginScript(options, oauth, redirect) {
|
|
|
310
371
|
var authBasePath=${authBasePath};var oauth=${oauth};var oauthQuery=location.search.slice(1);var redirectTarget=${redirectTarget};var errorEl=document.getElementById("error");
|
|
311
372
|
function showError(message){errorEl.textContent=message||"Sign-in failed. Please try again.";errorEl.style.display="block"}function clearError(){errorEl.style.display="none"}
|
|
312
373
|
async function post(path,body){var response=await fetch(authBasePath+path,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify(body)});var data={};try{data=await response.json()}catch{}if(!response.ok)throw new Error("Sign-in failed. Please try again.");return data}
|
|
313
|
-
function follow(data){var target=
|
|
374
|
+
function follow(data){var target=data.url||data.redirect_uri||redirectTarget;if(!target)throw new Error("Sign-in failed. Please try again.");location.assign(target)}
|
|
314
375
|
var emailForm=document.getElementById("email-form"),otpForm=document.getElementById("otp-form"),email="";
|
|
315
376
|
if(emailForm){emailForm.addEventListener("submit",async function(event){event.preventDefault();clearError();email=emailForm.querySelector("input").value;try{await post("/email-otp/send-verification-otp",{email:email,type:"sign-in",...(oauth?{oauth_query:oauthQuery}:{})});emailForm.hidden=true;otpForm.hidden=false}catch(error){showError(error.message)}});otpForm.addEventListener("submit",async function(event){event.preventDefault();clearError();try{var data=await post("/sign-in/email-otp",{email:email,otp:document.getElementById("otp").value,name:email.split("@")[0],...(oauth?{oauth_query:oauthQuery}:{})});follow(data)}catch(error){showError(error.message)}});document.getElementById("change-email").addEventListener("click",function(){otpForm.hidden=true;emailForm.hidden=false;clearError()})}
|
|
316
377
|
document.querySelectorAll("button.social").forEach(function(button){button.addEventListener("click",async function(){clearError();button.disabled=true;try{var body={provider:button.dataset.provider,...(oauth?{oauth_query:oauthQuery}:{callbackURL:redirectTarget,errorCallbackURL:location.href})};follow(await post("/sign-in/social",body))}catch(error){showError(error.message);button.disabled=false}})});
|
|
@@ -335,7 +396,8 @@ function createOAuthPagesRouter(rawOptions) {
|
|
|
335
396
|
response.type("html").send(
|
|
336
397
|
page(
|
|
337
398
|
`Sign in to ${options.appName}`,
|
|
338
|
-
loginMarkup(options) + loginScript(options, false, redirect)
|
|
399
|
+
loginMarkup(options) + loginScript(options, false, redirect),
|
|
400
|
+
options
|
|
339
401
|
)
|
|
340
402
|
);
|
|
341
403
|
});
|
|
@@ -345,19 +407,19 @@ function createOAuthPagesRouter(rawOptions) {
|
|
|
345
407
|
response.type("html").send(
|
|
346
408
|
page(
|
|
347
409
|
`Sign in to ${options.appName}`,
|
|
348
|
-
loginMarkup(options) + loginScript(options, true)
|
|
410
|
+
loginMarkup(options) + loginScript(options, true),
|
|
411
|
+
options
|
|
349
412
|
)
|
|
350
413
|
);
|
|
351
414
|
});
|
|
352
415
|
router.get("/oauth/consent", (request, response) => {
|
|
353
|
-
var _a;
|
|
354
416
|
frameProtection(response);
|
|
355
417
|
const clientId = typeof request.query.client_id === "string" ? request.query.client_id : "OAuth client";
|
|
356
418
|
const scope = typeof request.query.scope === "string" ? request.query.scope : "";
|
|
357
419
|
const scopes = scope.split(/\s+/).filter(Boolean);
|
|
358
420
|
const badges = scopes.map((item) => `<span class="scope">${escapeHtml(item)}</span>`).join("");
|
|
359
|
-
const body =
|
|
360
|
-
response.type("html").send(page(`Authorize ${options.appName} access`, body));
|
|
421
|
+
const body = `${brandMark(options)}<h1>Authorize ${escapeHtml(options.appName)} access</h1><p class="sub">Review the client and requested scopes.</p><div class="error" id="error"></div><div class="client"><div class="client-name">${escapeHtml(clientId)}</div>${badges ? `<div class="scopes">${badges}</div>` : ""}</div><div class="actions"><button class="primary" id="allow" type="button">Allow</button><button id="deny" type="button">Deny</button></div><script>(function(){var authBasePath=${scriptLiteral(options.authBasePath)},oauthQuery=location.search.slice(1),errorEl=document.getElementById("error"),buttons=[document.getElementById("allow"),document.getElementById("deny")];async function decide(accept){buttons.forEach(function(button){button.disabled=true});errorEl.style.display="none";try{var response=await fetch(authBasePath+"/oauth2/consent",{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({accept:accept,oauth_query:oauthQuery})});var data={};try{data=await response.json()}catch{}var target=data.url||data.redirect_uri;if(!response.ok||!target)throw new Error();location.assign(target)}catch{errorEl.textContent="Could not complete authorization.";errorEl.style.display="block";buttons.forEach(function(button){button.disabled=false})}}buttons[0].addEventListener("click",function(){decide(true)});buttons[1].addEventListener("click",function(){decide(false)})})();</script>`;
|
|
422
|
+
response.type("html").send(page(`Authorize ${options.appName} access`, body, options));
|
|
361
423
|
});
|
|
362
424
|
return router;
|
|
363
425
|
}
|
package/dist/express.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { __spreadProps, __spreadValues } from './chunk-
|
|
1
|
+
import { verifyOAuthAccessToken, validateOAuthVerificationOptions } from './chunk-JENFPLCB.mjs';
|
|
2
|
+
import { __objRest, __spreadProps, __spreadValues } from './chunk-FWCSY2DS.mjs';
|
|
3
3
|
import { Router } from 'express';
|
|
4
4
|
|
|
5
5
|
// src/express.ts
|
|
@@ -39,7 +39,8 @@ function bearerToken(request) {
|
|
|
39
39
|
return ((_a = match == null ? void 0 : match[1]) == null ? void 0 : _a.trim()) || null;
|
|
40
40
|
}
|
|
41
41
|
function createOAuthBearerMiddleware(options) {
|
|
42
|
-
|
|
42
|
+
const _a = options, { verifyAccessToken = verifyOAuthAccessToken } = _a, verificationOptions = __objRest(_a, ["verifyAccessToken"]);
|
|
43
|
+
validateOAuthVerificationOptions(verificationOptions);
|
|
43
44
|
if (options.resourceMetadataUrl) {
|
|
44
45
|
let metadataUrl;
|
|
45
46
|
try {
|
|
@@ -64,13 +65,9 @@ function createOAuthBearerMiddleware(options) {
|
|
|
64
65
|
);
|
|
65
66
|
return;
|
|
66
67
|
}
|
|
67
|
-
const result = await
|
|
68
|
+
const result = await verifyAccessToken(verificationOptions, token);
|
|
68
69
|
if (result.status !== "authenticated") {
|
|
69
|
-
sendAuthenticationError(
|
|
70
|
-
response,
|
|
71
|
-
result,
|
|
72
|
-
options.resourceMetadataUrl
|
|
73
|
-
);
|
|
70
|
+
sendAuthenticationError(response, result, options.resourceMetadataUrl);
|
|
74
71
|
return;
|
|
75
72
|
}
|
|
76
73
|
request.auth = result.identity;
|
|
@@ -106,6 +103,41 @@ function absoluteOrigin(value, field) {
|
|
|
106
103
|
}
|
|
107
104
|
return url.origin;
|
|
108
105
|
}
|
|
106
|
+
var HEX_COLOR = /^#(?:[0-9a-fA-F]{3,4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$/;
|
|
107
|
+
var OKLCH_COLOR = /^oklch\([0-9.%\s/,+-]+\)$/i;
|
|
108
|
+
function brandingColor(value, field) {
|
|
109
|
+
if (value === void 0) return void 0;
|
|
110
|
+
const color = value.trim();
|
|
111
|
+
if (HEX_COLOR.test(color) || OKLCH_COLOR.test(color)) return color;
|
|
112
|
+
throw new Error(`${field} must be a hex or oklch() color`);
|
|
113
|
+
}
|
|
114
|
+
function brandingUrl(value, field) {
|
|
115
|
+
if (value === void 0) return void 0;
|
|
116
|
+
const raw = value.trim();
|
|
117
|
+
if (!raw) throw new Error(`${field} must be a URL`);
|
|
118
|
+
if (raw.startsWith("/") && !raw.startsWith("//")) {
|
|
119
|
+
if (/\s/.test(raw) || raw.includes("\\") || raw.includes("<")) {
|
|
120
|
+
throw new Error(`${field} must be a safe URL`);
|
|
121
|
+
}
|
|
122
|
+
return raw;
|
|
123
|
+
}
|
|
124
|
+
let url;
|
|
125
|
+
try {
|
|
126
|
+
url = new URL(raw);
|
|
127
|
+
} catch (e) {
|
|
128
|
+
throw new Error(`${field} must be an absolute URL`);
|
|
129
|
+
}
|
|
130
|
+
if (!["http:", "https:"].includes(url.protocol)) {
|
|
131
|
+
throw new Error(`${field} must use http or https`);
|
|
132
|
+
}
|
|
133
|
+
if (process.env.NODE_ENV === "production" && url.protocol !== "https:") {
|
|
134
|
+
throw new Error(`${field} must use https in production`);
|
|
135
|
+
}
|
|
136
|
+
if (url.username || url.password) {
|
|
137
|
+
throw new Error(`${field} must not contain credentials`);
|
|
138
|
+
}
|
|
139
|
+
return url.toString();
|
|
140
|
+
}
|
|
109
141
|
function normalizeOptions(options) {
|
|
110
142
|
var _a;
|
|
111
143
|
if (!options.appName.trim()) throw new Error("appName is required");
|
|
@@ -145,9 +177,7 @@ function normalizeOptions(options) {
|
|
|
145
177
|
for (const method of options.loginMethods) {
|
|
146
178
|
if (method.type === "social") {
|
|
147
179
|
if (!method.providerId.trim() || !method.label.trim()) {
|
|
148
|
-
throw new Error(
|
|
149
|
-
"social login methods require providerId and label"
|
|
150
|
-
);
|
|
180
|
+
throw new Error("social login methods require providerId and label");
|
|
151
181
|
}
|
|
152
182
|
}
|
|
153
183
|
}
|
|
@@ -160,19 +190,42 @@ function normalizeOptions(options) {
|
|
|
160
190
|
appName: options.appName.trim(),
|
|
161
191
|
authBasePath,
|
|
162
192
|
allowedRedirectOrigins,
|
|
163
|
-
defaultRedirectUrl: defaultRedirect.toString()
|
|
193
|
+
defaultRedirectUrl: defaultRedirect.toString(),
|
|
194
|
+
logoUrl: brandingUrl(options.logoUrl, "logoUrl"),
|
|
195
|
+
faviconUrl: brandingUrl(options.faviconUrl, "faviconUrl"),
|
|
196
|
+
primaryColor: brandingColor(options.primaryColor, "primaryColor")
|
|
164
197
|
});
|
|
165
198
|
}
|
|
166
199
|
var STYLES = `
|
|
167
|
-
*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#
|
|
168
|
-
.card{width:100%;max-width:400px;padding:32px;border:1px solid #
|
|
169
|
-
h1{margin:0 0
|
|
170
|
-
form{margin:0}.field{margin-bottom:14px}label{display:block;margin-bottom:6px;font-size:12px}input{width:100%;padding:11px 13px;border:1px solid #
|
|
171
|
-
button{width:100%;margin-top:8px;padding:11px;border:1px solid #
|
|
172
|
-
|
|
200
|
+
html{--brand-primary:#8c7a6b;color-scheme:light}*{box-sizing:border-box}body{margin:0;min-height:100vh;display:grid;place-items:center;padding:24px;background:#f4f4f5;color:#171717;font-family:system-ui,sans-serif}
|
|
201
|
+
.card{width:100%;max-width:400px;padding:32px;border:1px solid #e4e4e7;border-radius:14px;background:#fff;box-shadow:0 1px 2px rgba(0,0,0,.04)}.logo{width:38px;height:38px;margin:0 auto 20px;display:grid;place-items:center;border-radius:50%;background:#171717;color:#fff;font-weight:700}.logo.has-image{background:transparent;border-radius:8px}.logo img{width:38px;height:38px;object-fit:contain;display:block}
|
|
202
|
+
h1{margin:0 0 24px;text-align:center;font-size:22px;font-weight:650}.sub{margin:-12px 0 24px;text-align:center;color:#71717a;font-size:13px;line-height:1.5}.error{display:none;margin-bottom:16px;padding:10px 12px;border-radius:8px;background:#fef2f2;color:#b42318;font-size:13px}
|
|
203
|
+
form{margin:0}.field{margin-bottom:14px}label{display:block;margin-bottom:6px;font-size:12px;color:#52525b}input{width:100%;padding:11px 13px;border:1px solid #d4d4d8;border-radius:8px;background:#fff;color:#171717;font-size:15px}input:focus{outline:2px solid var(--brand-primary);outline-offset:1px;border-color:transparent}
|
|
204
|
+
button{width:100%;margin-top:8px;padding:11px;border:1px solid #d4d4d8;border-radius:8px;background:#fafafa;color:#171717;font-size:14px;font-weight:550;cursor:pointer}button:focus-visible{outline:2px solid var(--brand-primary);outline-offset:2px}.primary{border-color:var(--brand-primary);background:var(--brand-primary);color:#fff}.secondary{color:#52525b;background:transparent}.social{display:inline-flex;align-items:center;justify-content:center;gap:10px;background:#fff;color:#1f1f1f;border-color:#dadce0;font-weight:500}.social:hover{background:#f7f8f8}.social svg{width:18px;height:18px;flex:0 0 18px}
|
|
205
|
+
.divider{display:flex;align-items:center;gap:10px;margin:18px 0;color:#a1a1aa;font-size:12px}.divider:before,.divider:after{content:"";flex:1;height:1px;background:#e4e4e7}
|
|
206
|
+
button:disabled{opacity:.55;cursor:not-allowed}.client{margin:20px 0;padding:14px;border:1px solid #e4e4e7;border-radius:9px;background:#fafafa}.client-name{font-weight:600;word-break:break-all}.scopes{display:flex;flex-wrap:wrap;gap:6px;margin-top:12px}.scope{padding:4px 7px;border:1px solid #e4e4e7;border-radius:6px;color:#52525b;font-size:12px}.actions{display:flex;gap:8px}.actions button{margin:0}
|
|
173
207
|
`;
|
|
174
|
-
function page(title, body) {
|
|
175
|
-
|
|
208
|
+
function page(title, body, options) {
|
|
209
|
+
const favicon = options.faviconUrl ? `<link rel="icon" href="${escapeHtml(options.faviconUrl)}">` : "";
|
|
210
|
+
const brandColor = options.primaryColor ? `html{--brand-primary:${options.primaryColor}}` : "";
|
|
211
|
+
return `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>${escapeHtml(title)}</title>${favicon}<style>${STYLES}${brandColor}</style></head><body><main class="card">${body}</main></body></html>`;
|
|
212
|
+
}
|
|
213
|
+
function brandMark(options) {
|
|
214
|
+
var _a;
|
|
215
|
+
if (options.logoUrl) {
|
|
216
|
+
return `<div class="logo has-image"><img src="${escapeHtml(options.logoUrl)}" alt="${escapeHtml(options.appName)}"></div>`;
|
|
217
|
+
}
|
|
218
|
+
return `<div class="logo">${escapeHtml((_a = options.appName[0]) != null ? _a : "?")}</div>`;
|
|
219
|
+
}
|
|
220
|
+
var SOCIAL_ICONS = {
|
|
221
|
+
google: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" aria-hidden="true"><path fill="#EA4335" d="M24 9.5c3.54 0 6.71 1.22 9.21 3.6l6.85-6.85C35.9 2.38 30.47 0 24 0 14.62 0 6.51 5.38 2.56 13.22l7.98 6.19C12.43 13.72 17.74 9.5 24 9.5z"/><path fill="#4285F4" d="M46.98 24.55c0-1.57-.15-3.09-.38-4.55H24v9.02h12.94c-.58 2.96-2.26 5.48-4.78 7.18l7.73 6c4.51-4.18 7.09-10.36 7.09-17.65z"/><path fill="#FBBC05" d="M10.53 28.59c-.48-1.45-.76-2.99-.76-4.59s.27-3.14.76-4.59l-7.98-6.19C.92 16.46 0 20.12 0 24c0 3.88.92 7.54 2.56 10.78l7.97-6.19z"/><path fill="#34A853" d="M24 48c6.48 0 11.93-2.13 15.89-5.81l-7.73-6c-2.15 1.45-4.92 2.3-8.16 2.3-6.26 0-11.57-4.22-13.47-9.91l-7.98 6.19C6.51 42.62 14.62 48 24 48z"/></svg>',
|
|
222
|
+
github: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor" aria-hidden="true"><path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82A7.7 7.7 0 0 1 8 4.84c.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.01 8.01 0 0 0 16 8c0-4.42-3.58-8-8-8"/></svg>',
|
|
223
|
+
microsoft: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 23 23" aria-hidden="true"><path fill="#f35325" d="M1 1h10v10H1z"/><path fill="#81bc06" d="M12 1h10v10H12z"/><path fill="#05a6f0" d="M1 12h10v10H1z"/><path fill="#ffba08" d="M12 12h10v10H12z"/></svg>'
|
|
224
|
+
};
|
|
225
|
+
function socialButton(method) {
|
|
226
|
+
var _a;
|
|
227
|
+
const icon = (_a = SOCIAL_ICONS[method.providerId]) != null ? _a : "";
|
|
228
|
+
return `<button type="button" class="social" data-provider="${escapeHtml(method.providerId)}">${icon}<span>${escapeHtml(method.label)}</span></button>`;
|
|
176
229
|
}
|
|
177
230
|
function clearLegacyCookies(response, names) {
|
|
178
231
|
for (const name of names != null ? names : []) {
|
|
@@ -186,19 +239,17 @@ function clearLegacyCookies(response, names) {
|
|
|
186
239
|
}
|
|
187
240
|
}
|
|
188
241
|
function loginMarkup(options) {
|
|
189
|
-
var _a
|
|
242
|
+
var _a;
|
|
190
243
|
const emailMethod = options.loginMethods.find(
|
|
191
244
|
(method) => method.type === "email-otp"
|
|
192
245
|
);
|
|
193
246
|
const socialMethods = options.loginMethods.filter(
|
|
194
247
|
(method) => method.type === "social"
|
|
195
248
|
);
|
|
196
|
-
const email = emailMethod ? `<form id="email-form"><div class="field"><label for="email">Email</label><input id="email" type="email" required autocomplete="email"></div><button class="primary" type="submit">${escapeHtml((_a = emailMethod.label) != null ? _a : "Continue with email")}</button></form><form id="otp-form" hidden><div class="field"><label for="otp">Verification code</label><input id="otp" inputmode="numeric" required autocomplete="one-time-code"></div><button class="primary" type="submit">Sign in</button><button class="secondary" type="button" id="change-email">Use a different email</button></form>` : "";
|
|
197
|
-
const social = socialMethods.map(
|
|
198
|
-
(method) => `<button type="button" class="social" data-provider="${escapeHtml(method.providerId)}">${escapeHtml(method.label)}</button>`
|
|
199
|
-
).join("");
|
|
249
|
+
const email = emailMethod ? `<form id="email-form"><div class="field"><label for="email">Email address</label><input id="email" type="email" required autocomplete="email"></div><button class="primary" type="submit">${escapeHtml((_a = emailMethod.label) != null ? _a : "Continue with email")}</button></form><form id="otp-form" hidden><div class="field"><label for="otp">Verification code</label><input id="otp" inputmode="numeric" required autocomplete="one-time-code"></div><button class="primary" type="submit">Sign in</button><button class="secondary" type="button" id="change-email">Use a different email</button></form>` : "";
|
|
250
|
+
const social = socialMethods.map(socialButton).join("");
|
|
200
251
|
const divider = email && social ? '<div class="divider">or</div>' : "";
|
|
201
|
-
return
|
|
252
|
+
return `${brandMark(options)}<h1>Sign in to ${escapeHtml(options.appName)}</h1><div class="error" id="error" role="alert"></div>${email}${divider}${social}`;
|
|
202
253
|
}
|
|
203
254
|
function loginScript(options, oauth, redirect) {
|
|
204
255
|
const authBasePath = scriptLiteral(options.authBasePath);
|
|
@@ -207,7 +258,7 @@ function loginScript(options, oauth, redirect) {
|
|
|
207
258
|
var authBasePath=${authBasePath};var oauth=${oauth};var oauthQuery=location.search.slice(1);var redirectTarget=${redirectTarget};var errorEl=document.getElementById("error");
|
|
208
259
|
function showError(message){errorEl.textContent=message||"Sign-in failed. Please try again.";errorEl.style.display="block"}function clearError(){errorEl.style.display="none"}
|
|
209
260
|
async function post(path,body){var response=await fetch(authBasePath+path,{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify(body)});var data={};try{data=await response.json()}catch{}if(!response.ok)throw new Error("Sign-in failed. Please try again.");return data}
|
|
210
|
-
function follow(data){var target=
|
|
261
|
+
function follow(data){var target=data.url||data.redirect_uri||redirectTarget;if(!target)throw new Error("Sign-in failed. Please try again.");location.assign(target)}
|
|
211
262
|
var emailForm=document.getElementById("email-form"),otpForm=document.getElementById("otp-form"),email="";
|
|
212
263
|
if(emailForm){emailForm.addEventListener("submit",async function(event){event.preventDefault();clearError();email=emailForm.querySelector("input").value;try{await post("/email-otp/send-verification-otp",{email:email,type:"sign-in",...(oauth?{oauth_query:oauthQuery}:{})});emailForm.hidden=true;otpForm.hidden=false}catch(error){showError(error.message)}});otpForm.addEventListener("submit",async function(event){event.preventDefault();clearError();try{var data=await post("/sign-in/email-otp",{email:email,otp:document.getElementById("otp").value,name:email.split("@")[0],...(oauth?{oauth_query:oauthQuery}:{})});follow(data)}catch(error){showError(error.message)}});document.getElementById("change-email").addEventListener("click",function(){otpForm.hidden=true;emailForm.hidden=false;clearError()})}
|
|
213
264
|
document.querySelectorAll("button.social").forEach(function(button){button.addEventListener("click",async function(){clearError();button.disabled=true;try{var body={provider:button.dataset.provider,...(oauth?{oauth_query:oauthQuery}:{callbackURL:redirectTarget,errorCallbackURL:location.href})};follow(await post("/sign-in/social",body))}catch(error){showError(error.message);button.disabled=false}})});
|
|
@@ -232,7 +283,8 @@ function createOAuthPagesRouter(rawOptions) {
|
|
|
232
283
|
response.type("html").send(
|
|
233
284
|
page(
|
|
234
285
|
`Sign in to ${options.appName}`,
|
|
235
|
-
loginMarkup(options) + loginScript(options, false, redirect)
|
|
286
|
+
loginMarkup(options) + loginScript(options, false, redirect),
|
|
287
|
+
options
|
|
236
288
|
)
|
|
237
289
|
);
|
|
238
290
|
});
|
|
@@ -242,19 +294,19 @@ function createOAuthPagesRouter(rawOptions) {
|
|
|
242
294
|
response.type("html").send(
|
|
243
295
|
page(
|
|
244
296
|
`Sign in to ${options.appName}`,
|
|
245
|
-
loginMarkup(options) + loginScript(options, true)
|
|
297
|
+
loginMarkup(options) + loginScript(options, true),
|
|
298
|
+
options
|
|
246
299
|
)
|
|
247
300
|
);
|
|
248
301
|
});
|
|
249
302
|
router.get("/oauth/consent", (request, response) => {
|
|
250
|
-
var _a;
|
|
251
303
|
frameProtection(response);
|
|
252
304
|
const clientId = typeof request.query.client_id === "string" ? request.query.client_id : "OAuth client";
|
|
253
305
|
const scope = typeof request.query.scope === "string" ? request.query.scope : "";
|
|
254
306
|
const scopes = scope.split(/\s+/).filter(Boolean);
|
|
255
307
|
const badges = scopes.map((item) => `<span class="scope">${escapeHtml(item)}</span>`).join("");
|
|
256
|
-
const body =
|
|
257
|
-
response.type("html").send(page(`Authorize ${options.appName} access`, body));
|
|
308
|
+
const body = `${brandMark(options)}<h1>Authorize ${escapeHtml(options.appName)} access</h1><p class="sub">Review the client and requested scopes.</p><div class="error" id="error"></div><div class="client"><div class="client-name">${escapeHtml(clientId)}</div>${badges ? `<div class="scopes">${badges}</div>` : ""}</div><div class="actions"><button class="primary" id="allow" type="button">Allow</button><button id="deny" type="button">Deny</button></div><script>(function(){var authBasePath=${scriptLiteral(options.authBasePath)},oauthQuery=location.search.slice(1),errorEl=document.getElementById("error"),buttons=[document.getElementById("allow"),document.getElementById("deny")];async function decide(accept){buttons.forEach(function(button){button.disabled=true});errorEl.style.display="none";try{var response=await fetch(authBasePath+"/oauth2/consent",{method:"POST",headers:{"content-type":"application/json",accept:"application/json"},body:JSON.stringify({accept:accept,oauth_query:oauthQuery})});var data={};try{data=await response.json()}catch{}var target=data.url||data.redirect_uri;if(!response.ok||!target)throw new Error();location.assign(target)}catch{errorEl.textContent="Could not complete authorization.";errorEl.style.display="block";buttons.forEach(function(button){button.disabled=false})}}buttons[0].addEventListener("click",function(){decide(true)});buttons[1].addEventListener("click",function(){decide(false)})})();</script>`;
|
|
309
|
+
response.type("html").send(page(`Authorize ${options.appName} access`, body, options));
|
|
258
310
|
});
|
|
259
311
|
return router;
|
|
260
312
|
}
|
package/dist/index.js
CHANGED
|
@@ -64,16 +64,14 @@ async function verifyOAuthAccessToken(options, token) {
|
|
|
64
64
|
const audiences = normalizedUniqueStrings(options.audiences);
|
|
65
65
|
if (!token.trim()) return { status: "invalid_token" };
|
|
66
66
|
try {
|
|
67
|
-
const claims = await options.oauthResourceClient.getActions().
|
|
67
|
+
const claims = await options.oauthResourceClient.getActions().verifyBearerToken(token, {
|
|
68
68
|
verifyOptions: { issuer, audience: audiences },
|
|
69
69
|
resourceMetadataMappings: (_a = options.resourceMetadataMappings) != null ? _a : {}
|
|
70
70
|
});
|
|
71
71
|
const subject = optionalString(claims.sub);
|
|
72
72
|
const claimIssuer = optionalString(claims.iss);
|
|
73
73
|
const clientId = (_b = optionalString(claims.azp)) != null ? _b : optionalString(claims.client_id);
|
|
74
|
-
const tokenAudiences = normalizedUniqueStrings(
|
|
75
|
-
claimStrings(claims.aud)
|
|
76
|
-
);
|
|
74
|
+
const tokenAudiences = normalizedUniqueStrings(claimStrings(claims.aud));
|
|
77
75
|
const validatedAudiences = tokenAudiences.filter(
|
|
78
76
|
(audience) => audiences.includes(audience)
|
|
79
77
|
);
|
package/dist/index.mjs
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export { verifyOAuthAccessToken } from './chunk-
|
|
2
|
-
import './chunk-
|
|
1
|
+
export { verifyOAuthAccessToken } from './chunk-JENFPLCB.mjs';
|
|
2
|
+
import './chunk-FWCSY2DS.mjs';
|
package/dist/mcp.d.mts
CHANGED
|
@@ -18,6 +18,10 @@ interface CreateMcpOAuthDiscoveryRoutesOptions {
|
|
|
18
18
|
resourceUrl: string;
|
|
19
19
|
scopesSupported: readonly string[];
|
|
20
20
|
allowedOrigins: readonly string[] | "*";
|
|
21
|
+
metadataHandlers?: {
|
|
22
|
+
authorizationServer: (auth: BetterAuthMetadataApi) => (request: Request) => Promise<Response>;
|
|
23
|
+
openId: (auth: BetterAuthMetadataApi) => (request: Request) => Promise<Response>;
|
|
24
|
+
};
|
|
21
25
|
}
|
|
22
26
|
declare function createMcpOAuthDiscoveryRoutes(rawOptions: CreateMcpOAuthDiscoveryRoutesOptions): Router;
|
|
23
27
|
|
package/dist/mcp.d.ts
CHANGED
|
@@ -18,6 +18,10 @@ interface CreateMcpOAuthDiscoveryRoutesOptions {
|
|
|
18
18
|
resourceUrl: string;
|
|
19
19
|
scopesSupported: readonly string[];
|
|
20
20
|
allowedOrigins: readonly string[] | "*";
|
|
21
|
+
metadataHandlers?: {
|
|
22
|
+
authorizationServer: (auth: BetterAuthMetadataApi) => (request: Request) => Promise<Response>;
|
|
23
|
+
openId: (auth: BetterAuthMetadataApi) => (request: Request) => Promise<Response>;
|
|
24
|
+
};
|
|
21
25
|
}
|
|
22
26
|
declare function createMcpOAuthDiscoveryRoutes(rawOptions: CreateMcpOAuthDiscoveryRoutesOptions): Router;
|
|
23
27
|
|
package/dist/mcp.js
CHANGED
|
@@ -33,9 +33,7 @@ function normalizeOptions(options) {
|
|
|
33
33
|
throw new Error("resourceUrl must use http or https");
|
|
34
34
|
}
|
|
35
35
|
if (resource.username || resource.password || resource.search || resource.hash) {
|
|
36
|
-
throw new Error(
|
|
37
|
-
"resourceUrl must not include credentials, query, or fragment"
|
|
38
|
-
);
|
|
36
|
+
throw new Error("resourceUrl must not include credentials, query, or fragment");
|
|
39
37
|
}
|
|
40
38
|
if (process.env.NODE_ENV === "production" && resource.protocol !== "https:") {
|
|
41
39
|
throw new Error("resourceUrl must use https in production");
|
|
@@ -65,9 +63,7 @@ function normalizeOptions(options) {
|
|
|
65
63
|
const authorizationServerPath = rawAuthorizationServerPath === "/" ? "" : rawAuthorizationServerPath.replace(/\/+$/, "");
|
|
66
64
|
const authorizationServerUrl = `${authorizationServerBase.toString().replace(/\/$/, "")}${authorizationServerPath}`;
|
|
67
65
|
const scopesSupported = [
|
|
68
|
-
...new Set(
|
|
69
|
-
options.scopesSupported.map((scope) => scope.trim()).filter(Boolean)
|
|
70
|
-
)
|
|
66
|
+
...new Set(options.scopesSupported.map((scope) => scope.trim()).filter(Boolean))
|
|
71
67
|
];
|
|
72
68
|
const allowedOrigins = options.allowedOrigins === "*" ? "*" : new Set(
|
|
73
69
|
options.allowedOrigins.map((origin) => {
|
|
@@ -75,24 +71,16 @@ function normalizeOptions(options) {
|
|
|
75
71
|
try {
|
|
76
72
|
url = new URL(origin);
|
|
77
73
|
} catch (e) {
|
|
78
|
-
throw new Error(
|
|
79
|
-
"allowedOrigins must contain absolute origins"
|
|
80
|
-
);
|
|
74
|
+
throw new Error("allowedOrigins must contain absolute origins");
|
|
81
75
|
}
|
|
82
76
|
if (!["http:", "https:"].includes(url.protocol)) {
|
|
83
|
-
throw new Error(
|
|
84
|
-
"allowedOrigins must use http or https"
|
|
85
|
-
);
|
|
77
|
+
throw new Error("allowedOrigins must use http or https");
|
|
86
78
|
}
|
|
87
79
|
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
88
|
-
throw new Error(
|
|
89
|
-
"allowedOrigins must contain origins without paths"
|
|
90
|
-
);
|
|
80
|
+
throw new Error("allowedOrigins must contain origins without paths");
|
|
91
81
|
}
|
|
92
82
|
if (process.env.NODE_ENV === "production" && url.protocol !== "https:") {
|
|
93
|
-
throw new Error(
|
|
94
|
-
"allowedOrigins must use https in production"
|
|
95
|
-
);
|
|
83
|
+
throw new Error("allowedOrigins must use https in production");
|
|
96
84
|
}
|
|
97
85
|
return url.origin;
|
|
98
86
|
})
|
|
@@ -128,9 +116,9 @@ function applyCors(request, response, allowedOrigins) {
|
|
|
128
116
|
}
|
|
129
117
|
async function sendFetchResponse(response, fetchResponse) {
|
|
130
118
|
response.status(fetchResponse.status);
|
|
131
|
-
fetchResponse.headers.forEach(
|
|
132
|
-
|
|
133
|
-
);
|
|
119
|
+
fetchResponse.headers.forEach((value, key) => {
|
|
120
|
+
response.setHeader(key, value);
|
|
121
|
+
});
|
|
134
122
|
response.send(await fetchResponse.text());
|
|
135
123
|
}
|
|
136
124
|
function asyncHandler(handler) {
|
|
@@ -149,10 +137,7 @@ function createMcpOAuthDiscoveryRoutes(rawOptions) {
|
|
|
149
137
|
"/.well-known/openid-configuration",
|
|
150
138
|
`${options.authorizationServerPath}/.well-known/openid-configuration`
|
|
151
139
|
]);
|
|
152
|
-
const resourcePath = new URL(options.resourceUrl).pathname.replace(
|
|
153
|
-
/\/$/,
|
|
154
|
-
""
|
|
155
|
-
);
|
|
140
|
+
const resourcePath = new URL(options.resourceUrl).pathname.replace(/\/$/, "");
|
|
156
141
|
const protectedResourcePaths = /* @__PURE__ */ new Set([
|
|
157
142
|
"/.well-known/oauth-protected-resource",
|
|
158
143
|
`/.well-known/oauth-protected-resource${resourcePath}`
|
|
@@ -178,7 +163,8 @@ function createMcpOAuthDiscoveryRoutes(rawOptions) {
|
|
|
178
163
|
next();
|
|
179
164
|
});
|
|
180
165
|
async function authorizationServerMetadata(request, response) {
|
|
181
|
-
|
|
166
|
+
var _a, _b;
|
|
167
|
+
const handler = ((_b = (_a = options.metadataHandlers) == null ? void 0 : _a.authorizationServer) != null ? _b : oauthProvider.oauthProviderAuthServerMetadata)(options.auth);
|
|
182
168
|
await sendFetchResponse(
|
|
183
169
|
response,
|
|
184
170
|
await handler(
|
|
@@ -190,7 +176,8 @@ function createMcpOAuthDiscoveryRoutes(rawOptions) {
|
|
|
190
176
|
);
|
|
191
177
|
}
|
|
192
178
|
async function openIdMetadata(request, response) {
|
|
193
|
-
|
|
179
|
+
var _a, _b;
|
|
180
|
+
const handler = ((_b = (_a = options.metadataHandlers) == null ? void 0 : _a.openId) != null ? _b : oauthProvider.oauthProviderOpenIdConfigMetadata)(options.auth);
|
|
194
181
|
await sendFetchResponse(
|
|
195
182
|
response,
|
|
196
183
|
await handler(
|
|
@@ -208,15 +195,12 @@ function createMcpOAuthDiscoveryRoutes(rawOptions) {
|
|
|
208
195
|
router.get(path, asyncHandler(openIdMetadata));
|
|
209
196
|
}
|
|
210
197
|
async function protectedResourceMetadata(_request, response) {
|
|
211
|
-
const metadata = await options.oauthResourceClient.getActions().getProtectedResourceMetadata(
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
},
|
|
218
|
-
{ silenceWarnings: { oidcScopes: true } }
|
|
219
|
-
);
|
|
198
|
+
const metadata = await options.oauthResourceClient.getActions().getProtectedResourceMetadata({
|
|
199
|
+
resource: options.resourceUrl,
|
|
200
|
+
authorization_servers: [options.authorizationServerUrl],
|
|
201
|
+
scopes_supported: options.scopesSupported,
|
|
202
|
+
bearer_methods_supported: ["header"]
|
|
203
|
+
});
|
|
220
204
|
response.json(metadata);
|
|
221
205
|
}
|
|
222
206
|
for (const path of protectedResourcePaths) {
|
package/dist/mcp.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { __spreadProps, __spreadValues } from './chunk-
|
|
1
|
+
import { __spreadProps, __spreadValues } from './chunk-FWCSY2DS.mjs';
|
|
2
2
|
import { oauthProviderAuthServerMetadata, oauthProviderOpenIdConfigMetadata } from '@better-auth/oauth-provider';
|
|
3
3
|
import { Router } from 'express';
|
|
4
4
|
|
|
@@ -13,9 +13,7 @@ function normalizeOptions(options) {
|
|
|
13
13
|
throw new Error("resourceUrl must use http or https");
|
|
14
14
|
}
|
|
15
15
|
if (resource.username || resource.password || resource.search || resource.hash) {
|
|
16
|
-
throw new Error(
|
|
17
|
-
"resourceUrl must not include credentials, query, or fragment"
|
|
18
|
-
);
|
|
16
|
+
throw new Error("resourceUrl must not include credentials, query, or fragment");
|
|
19
17
|
}
|
|
20
18
|
if (process.env.NODE_ENV === "production" && resource.protocol !== "https:") {
|
|
21
19
|
throw new Error("resourceUrl must use https in production");
|
|
@@ -45,9 +43,7 @@ function normalizeOptions(options) {
|
|
|
45
43
|
const authorizationServerPath = rawAuthorizationServerPath === "/" ? "" : rawAuthorizationServerPath.replace(/\/+$/, "");
|
|
46
44
|
const authorizationServerUrl = `${authorizationServerBase.toString().replace(/\/$/, "")}${authorizationServerPath}`;
|
|
47
45
|
const scopesSupported = [
|
|
48
|
-
...new Set(
|
|
49
|
-
options.scopesSupported.map((scope) => scope.trim()).filter(Boolean)
|
|
50
|
-
)
|
|
46
|
+
...new Set(options.scopesSupported.map((scope) => scope.trim()).filter(Boolean))
|
|
51
47
|
];
|
|
52
48
|
const allowedOrigins = options.allowedOrigins === "*" ? "*" : new Set(
|
|
53
49
|
options.allowedOrigins.map((origin) => {
|
|
@@ -55,24 +51,16 @@ function normalizeOptions(options) {
|
|
|
55
51
|
try {
|
|
56
52
|
url = new URL(origin);
|
|
57
53
|
} catch (e) {
|
|
58
|
-
throw new Error(
|
|
59
|
-
"allowedOrigins must contain absolute origins"
|
|
60
|
-
);
|
|
54
|
+
throw new Error("allowedOrigins must contain absolute origins");
|
|
61
55
|
}
|
|
62
56
|
if (!["http:", "https:"].includes(url.protocol)) {
|
|
63
|
-
throw new Error(
|
|
64
|
-
"allowedOrigins must use http or https"
|
|
65
|
-
);
|
|
57
|
+
throw new Error("allowedOrigins must use http or https");
|
|
66
58
|
}
|
|
67
59
|
if (url.username || url.password || url.pathname !== "/" || url.search || url.hash) {
|
|
68
|
-
throw new Error(
|
|
69
|
-
"allowedOrigins must contain origins without paths"
|
|
70
|
-
);
|
|
60
|
+
throw new Error("allowedOrigins must contain origins without paths");
|
|
71
61
|
}
|
|
72
62
|
if (process.env.NODE_ENV === "production" && url.protocol !== "https:") {
|
|
73
|
-
throw new Error(
|
|
74
|
-
"allowedOrigins must use https in production"
|
|
75
|
-
);
|
|
63
|
+
throw new Error("allowedOrigins must use https in production");
|
|
76
64
|
}
|
|
77
65
|
return url.origin;
|
|
78
66
|
})
|
|
@@ -108,9 +96,9 @@ function applyCors(request, response, allowedOrigins) {
|
|
|
108
96
|
}
|
|
109
97
|
async function sendFetchResponse(response, fetchResponse) {
|
|
110
98
|
response.status(fetchResponse.status);
|
|
111
|
-
fetchResponse.headers.forEach(
|
|
112
|
-
|
|
113
|
-
);
|
|
99
|
+
fetchResponse.headers.forEach((value, key) => {
|
|
100
|
+
response.setHeader(key, value);
|
|
101
|
+
});
|
|
114
102
|
response.send(await fetchResponse.text());
|
|
115
103
|
}
|
|
116
104
|
function asyncHandler(handler) {
|
|
@@ -129,10 +117,7 @@ function createMcpOAuthDiscoveryRoutes(rawOptions) {
|
|
|
129
117
|
"/.well-known/openid-configuration",
|
|
130
118
|
`${options.authorizationServerPath}/.well-known/openid-configuration`
|
|
131
119
|
]);
|
|
132
|
-
const resourcePath = new URL(options.resourceUrl).pathname.replace(
|
|
133
|
-
/\/$/,
|
|
134
|
-
""
|
|
135
|
-
);
|
|
120
|
+
const resourcePath = new URL(options.resourceUrl).pathname.replace(/\/$/, "");
|
|
136
121
|
const protectedResourcePaths = /* @__PURE__ */ new Set([
|
|
137
122
|
"/.well-known/oauth-protected-resource",
|
|
138
123
|
`/.well-known/oauth-protected-resource${resourcePath}`
|
|
@@ -158,7 +143,8 @@ function createMcpOAuthDiscoveryRoutes(rawOptions) {
|
|
|
158
143
|
next();
|
|
159
144
|
});
|
|
160
145
|
async function authorizationServerMetadata(request, response) {
|
|
161
|
-
|
|
146
|
+
var _a, _b;
|
|
147
|
+
const handler = ((_b = (_a = options.metadataHandlers) == null ? void 0 : _a.authorizationServer) != null ? _b : oauthProviderAuthServerMetadata)(options.auth);
|
|
162
148
|
await sendFetchResponse(
|
|
163
149
|
response,
|
|
164
150
|
await handler(
|
|
@@ -170,7 +156,8 @@ function createMcpOAuthDiscoveryRoutes(rawOptions) {
|
|
|
170
156
|
);
|
|
171
157
|
}
|
|
172
158
|
async function openIdMetadata(request, response) {
|
|
173
|
-
|
|
159
|
+
var _a, _b;
|
|
160
|
+
const handler = ((_b = (_a = options.metadataHandlers) == null ? void 0 : _a.openId) != null ? _b : oauthProviderOpenIdConfigMetadata)(options.auth);
|
|
174
161
|
await sendFetchResponse(
|
|
175
162
|
response,
|
|
176
163
|
await handler(
|
|
@@ -188,15 +175,12 @@ function createMcpOAuthDiscoveryRoutes(rawOptions) {
|
|
|
188
175
|
router.get(path, asyncHandler(openIdMetadata));
|
|
189
176
|
}
|
|
190
177
|
async function protectedResourceMetadata(_request, response) {
|
|
191
|
-
const metadata = await options.oauthResourceClient.getActions().getProtectedResourceMetadata(
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
},
|
|
198
|
-
{ silenceWarnings: { oidcScopes: true } }
|
|
199
|
-
);
|
|
178
|
+
const metadata = await options.oauthResourceClient.getActions().getProtectedResourceMetadata({
|
|
179
|
+
resource: options.resourceUrl,
|
|
180
|
+
authorization_servers: [options.authorizationServerUrl],
|
|
181
|
+
scopes_supported: options.scopesSupported,
|
|
182
|
+
bearer_methods_supported: ["header"]
|
|
183
|
+
});
|
|
200
184
|
response.json(metadata);
|
|
201
185
|
}
|
|
202
186
|
for (const path of protectedResourcePaths) {
|
package/package.json
CHANGED
|
@@ -1,101 +1,101 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
"
|
|
9
|
-
"
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
32
|
-
},
|
|
33
|
-
"./express": {
|
|
34
|
-
"import": {
|
|
35
|
-
"types": "./dist/express.d.mts",
|
|
36
|
-
"default": "./dist/express.mjs"
|
|
37
|
-
},
|
|
38
|
-
"require": {
|
|
39
|
-
"types": "./dist/express.d.ts",
|
|
40
|
-
"default": "./dist/express.js"
|
|
41
|
-
}
|
|
42
|
-
},
|
|
43
|
-
"./mcp": {
|
|
44
|
-
"import": {
|
|
45
|
-
"types": "./dist/mcp.d.mts",
|
|
46
|
-
"default": "./dist/mcp.mjs"
|
|
47
|
-
},
|
|
48
|
-
"require": {
|
|
49
|
-
"types": "./dist/mcp.d.ts",
|
|
50
|
-
"default": "./dist/mcp.js"
|
|
51
|
-
}
|
|
52
|
-
}
|
|
2
|
+
"name": "@codelitdev/oauth-server-kit",
|
|
3
|
+
"version": "0.1.0-alpha.2",
|
|
4
|
+
"description": "Lean Better Auth OAuth authentication framework for web, mobile, REST, and MCP surfaces.",
|
|
5
|
+
"author": "CodeLit",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "git+https://github.com/codelitdev/platform.git",
|
|
10
|
+
"directory": "packages/oauth-server-kit"
|
|
11
|
+
},
|
|
12
|
+
"homepage": "https://github.com/codelitdev/platform/tree/main/packages/oauth-server-kit#readme",
|
|
13
|
+
"bugs": {
|
|
14
|
+
"url": "https://github.com/codelitdev/platform/issues"
|
|
15
|
+
},
|
|
16
|
+
"main": "dist/index.js",
|
|
17
|
+
"types": "dist/index.d.ts",
|
|
18
|
+
"files": [
|
|
19
|
+
"dist"
|
|
20
|
+
],
|
|
21
|
+
"exports": {
|
|
22
|
+
".": {
|
|
23
|
+
"import": {
|
|
24
|
+
"types": "./dist/index.d.mts",
|
|
25
|
+
"default": "./dist/index.mjs"
|
|
26
|
+
},
|
|
27
|
+
"require": {
|
|
28
|
+
"types": "./dist/index.d.ts",
|
|
29
|
+
"default": "./dist/index.js"
|
|
30
|
+
}
|
|
53
31
|
},
|
|
54
|
-
"
|
|
55
|
-
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
"
|
|
61
|
-
"
|
|
62
|
-
|
|
63
|
-
"lint:fix": "pnpm lint --fix",
|
|
64
|
-
"prettier": "prettier --write src test docs *.md *.json",
|
|
65
|
-
"format": "pnpm prettier",
|
|
66
|
-
"format:check": "prettier --check src test docs *.md *.json",
|
|
67
|
-
"quality": "pnpm format:check && pnpm lint && pnpm check-types",
|
|
68
|
-
"prepare": "husky",
|
|
69
|
-
"release:alpha": "pnpm quality && pnpm test:coverage && pnpm build && npm publish --tag alpha"
|
|
32
|
+
"./better-auth": {
|
|
33
|
+
"import": {
|
|
34
|
+
"types": "./dist/better-auth.d.mts",
|
|
35
|
+
"default": "./dist/better-auth.mjs"
|
|
36
|
+
},
|
|
37
|
+
"require": {
|
|
38
|
+
"types": "./dist/better-auth.d.ts",
|
|
39
|
+
"default": "./dist/better-auth.js"
|
|
40
|
+
}
|
|
70
41
|
},
|
|
71
|
-
"
|
|
72
|
-
|
|
73
|
-
"
|
|
74
|
-
"
|
|
42
|
+
"./express": {
|
|
43
|
+
"import": {
|
|
44
|
+
"types": "./dist/express.d.mts",
|
|
45
|
+
"default": "./dist/express.mjs"
|
|
46
|
+
},
|
|
47
|
+
"require": {
|
|
48
|
+
"types": "./dist/express.d.ts",
|
|
49
|
+
"default": "./dist/express.js"
|
|
50
|
+
}
|
|
75
51
|
},
|
|
76
|
-
"
|
|
77
|
-
|
|
78
|
-
"
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
"
|
|
83
|
-
"
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
52
|
+
"./mcp": {
|
|
53
|
+
"import": {
|
|
54
|
+
"types": "./dist/mcp.d.mts",
|
|
55
|
+
"default": "./dist/mcp.mjs"
|
|
56
|
+
},
|
|
57
|
+
"require": {
|
|
58
|
+
"types": "./dist/mcp.d.ts",
|
|
59
|
+
"default": "./dist/mcp.js"
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
},
|
|
63
|
+
"scripts": {
|
|
64
|
+
"clean": "rimraf dist/",
|
|
65
|
+
"build": "tsup",
|
|
66
|
+
"dev": "tsup --watch",
|
|
67
|
+
"check-types": "tsc -p tsconfig.test.json --noEmit",
|
|
68
|
+
"test": "bun test --timeout 60000 src",
|
|
69
|
+
"test:watch": "bun test --watch --timeout 60000 src",
|
|
70
|
+
"test:coverage": "bun test --coverage --timeout 60000 src",
|
|
71
|
+
"lint": "biome lint .",
|
|
72
|
+
"lint:fix": "biome lint --write .",
|
|
73
|
+
"format": "biome format --write .",
|
|
74
|
+
"format:check": "biome ci --linter-enabled=false .",
|
|
75
|
+
"quality": "bun run format:check && bun run lint && bun run check-types"
|
|
76
|
+
},
|
|
77
|
+
"publishConfig": {
|
|
78
|
+
"access": "public",
|
|
79
|
+
"tag": "latest"
|
|
80
|
+
},
|
|
81
|
+
"peerDependencies": {
|
|
82
|
+
"@better-auth/oauth-provider": "^1.7.2",
|
|
83
|
+
"better-auth": "^1.7.2",
|
|
84
|
+
"express": "^4.21.2"
|
|
85
|
+
},
|
|
86
|
+
"devDependencies": {
|
|
87
|
+
"@better-auth/core": "1.7.2",
|
|
88
|
+
"@better-auth/oauth-provider": "1.7.2",
|
|
89
|
+
"@better-auth/utils": "0.4.2",
|
|
90
|
+
"@better-fetch/fetch": "1.3.1",
|
|
91
|
+
"@types/express": "^4.17.20",
|
|
92
|
+
"@types/node": "^22.14.1",
|
|
93
|
+
"@types/bun": "^1.4.1",
|
|
94
|
+
"better-auth": "1.7.2",
|
|
95
|
+
"better-call": "1.4.0",
|
|
96
|
+
"express": "^4.21.2",
|
|
97
|
+
"rimraf": "^5.0.0",
|
|
98
|
+
"tsup": "^8.3.0",
|
|
99
|
+
"typescript": "^5.9.3"
|
|
100
|
+
}
|
|
101
101
|
}
|
package/LICENSE
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
MIT License
|
|
2
|
-
|
|
3
|
-
Copyright (c) 2026 CodeLit
|
|
4
|
-
|
|
5
|
-
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
-
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
-
in the Software without restriction, including without limitation the rights
|
|
8
|
-
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
-
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
-
furnished to do so, subject to the following conditions:
|
|
11
|
-
|
|
12
|
-
The above copyright notice and this permission notice shall be included in all
|
|
13
|
-
copies or substantial portions of the Software.
|
|
14
|
-
|
|
15
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
-
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
-
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
-
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
-
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
-
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
-
SOFTWARE.
|