@keycardai/express 0.4.0 → 0.5.0
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/cjs/grant.d.ts +23 -1
- package/dist/cjs/grant.d.ts.map +1 -1
- package/dist/cjs/grant.js +45 -6
- package/dist/cjs/grant.js.map +1 -1
- package/dist/esm/grant.d.ts +23 -1
- package/dist/esm/grant.d.ts.map +1 -1
- package/dist/esm/grant.js +45 -6
- package/dist/esm/grant.js.map +1 -1
- package/package.json +1 -1
package/dist/cjs/grant.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RequestHandler } from "express";
|
|
1
|
+
import type { Request, RequestHandler } from "express";
|
|
2
2
|
import { AccessContext } from "@keycardai/oauth/server/accessContext";
|
|
3
3
|
import type { ApplicationCredential } from "@keycardai/oauth/credentials";
|
|
4
4
|
import type { AuthenticatedRequest } from "./bearerAuth.js";
|
|
@@ -33,6 +33,22 @@ export interface GrantOptions {
|
|
|
33
33
|
* are selected per request.
|
|
34
34
|
*/
|
|
35
35
|
applicationCredential?: ApplicationCredential;
|
|
36
|
+
/**
|
|
37
|
+
* Resolver for the user identity to impersonate. When set, each
|
|
38
|
+
* per-resource exchange uses the substitute-user impersonation flow
|
|
39
|
+
* (`TokenExchangeClient.impersonate`) with the resolved identifier
|
|
40
|
+
* instead of exchanging the caller's bearer token. The resolver runs
|
|
41
|
+
* once per request and may be async.
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* grant(resources, {
|
|
45
|
+
* zoneUrl,
|
|
46
|
+
* applicationCredential,
|
|
47
|
+
* userIdentifier: (req) => req.auth.subject,
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
userIdentifier?: (req: Request) => string | Promise<string>;
|
|
36
52
|
}
|
|
37
53
|
/**
|
|
38
54
|
* Express middleware factory for delegated token exchange (RFC 8693).
|
|
@@ -45,6 +61,12 @@ export interface GrantOptions {
|
|
|
45
61
|
* `TokenResponse` for that resource. On partial failure, some resources
|
|
46
62
|
* may have errors while others succeed.
|
|
47
63
|
*
|
|
64
|
+
* If the request carries no verified bearer token, responds 401 with an
|
|
65
|
+
* RFC 6750 `WWW-Authenticate` challenge; the handler does not run.
|
|
66
|
+
*
|
|
67
|
+
* Multiple `grant()` middlewares may be stacked: each merges its
|
|
68
|
+
* per-resource tokens and errors into the existing `req.accessContext`.
|
|
69
|
+
*
|
|
48
70
|
* ```ts
|
|
49
71
|
* app.use(requireBearerAuth({ issuer: "https://zone.keycard.cloud" }));
|
|
50
72
|
* app.use(grant(["https://graph.microsoft.com"], { zoneUrl: "https://zone.keycard.cloud" }));
|
package/dist/cjs/grant.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../src/grant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../src/grant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA0B,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAGvE,MAAM,WAAW,cAAe,SAAQ,oBAAoB;IAC1D,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,MAAM,CAAC,CAAC;IAClD;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IAC9C;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,KAAK,CACnB,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACrC,OAAO,EAAE,YAAY,GACpB,cAAc,CAyIhB"}
|
package/dist/cjs/grant.js
CHANGED
|
@@ -15,6 +15,12 @@ const errors_1 = require("@keycardai/oauth/errors");
|
|
|
15
15
|
* `TokenResponse` for that resource. On partial failure, some resources
|
|
16
16
|
* may have errors while others succeed.
|
|
17
17
|
*
|
|
18
|
+
* If the request carries no verified bearer token, responds 401 with an
|
|
19
|
+
* RFC 6750 `WWW-Authenticate` challenge; the handler does not run.
|
|
20
|
+
*
|
|
21
|
+
* Multiple `grant()` middlewares may be stacked: each merges its
|
|
22
|
+
* per-resource tokens and errors into the existing `req.accessContext`.
|
|
23
|
+
*
|
|
18
24
|
* ```ts
|
|
19
25
|
* app.use(requireBearerAuth({ issuer: "https://zone.keycard.cloud" }));
|
|
20
26
|
* app.use(grant(["https://graph.microsoft.com"], { zoneUrl: "https://zone.keycard.cloud" }));
|
|
@@ -35,17 +41,25 @@ function grant(resources, options) {
|
|
|
35
41
|
// requests — the TokenExchangeClient already caches the token_endpoint
|
|
36
42
|
// internally after the first discovery call.
|
|
37
43
|
const clientCache = new Map();
|
|
38
|
-
return async (req,
|
|
44
|
+
return async (req, res, next) => {
|
|
39
45
|
const authReq = req;
|
|
40
46
|
const subjectToken = authReq.auth?.token;
|
|
41
|
-
const accessCtx = new accessContext_1.AccessContext();
|
|
42
47
|
if (!subjectToken) {
|
|
43
|
-
|
|
44
|
-
|
|
48
|
+
// Unauthenticated request: respond 401 with an RFC 6750 challenge
|
|
49
|
+
// (same shape as requireBearerAuth) without invoking the handler.
|
|
50
|
+
const resourceMetadataUrl = `${req.protocol}://${req.host}/.well-known/oauth-protected-resource`;
|
|
51
|
+
res.set("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
|
|
52
|
+
res.status(401).json({
|
|
53
|
+
error: "invalid_request",
|
|
54
|
+
error_description: "Missing bearer token. Ensure requireBearerAuth() runs before grant().",
|
|
45
55
|
});
|
|
46
|
-
|
|
47
|
-
return next();
|
|
56
|
+
return;
|
|
48
57
|
}
|
|
58
|
+
// Reuse an existing context so stacked grant() middlewares accumulate
|
|
59
|
+
// per-resource tokens and errors instead of replacing earlier results.
|
|
60
|
+
const accessCtx = req.accessContext instanceof accessContext_1.AccessContext
|
|
61
|
+
? req.accessContext
|
|
62
|
+
: new accessContext_1.AccessContext();
|
|
49
63
|
// Resolve zone at request time — zoneId may be a static string or a
|
|
50
64
|
// function that extracts the zone from the verified access token.
|
|
51
65
|
let resolvedZoneId;
|
|
@@ -79,8 +93,33 @@ function grant(resources, options) {
|
|
|
79
93
|
? resources
|
|
80
94
|
: [resources];
|
|
81
95
|
const tokens = {};
|
|
96
|
+
// Resolve the impersonation target once per request. A resolver failure
|
|
97
|
+
// is recorded as a global error; the handler still runs and access()
|
|
98
|
+
// surfaces the failure.
|
|
99
|
+
let resolvedUserIdentifier;
|
|
100
|
+
if (options.userIdentifier) {
|
|
101
|
+
try {
|
|
102
|
+
resolvedUserIdentifier = await options.userIdentifier(req);
|
|
103
|
+
}
|
|
104
|
+
catch (e) {
|
|
105
|
+
accessCtx.setError({
|
|
106
|
+
message: "Failed to resolve userIdentifier.",
|
|
107
|
+
rawError: String(e),
|
|
108
|
+
});
|
|
109
|
+
req.accessContext = accessCtx;
|
|
110
|
+
return next();
|
|
111
|
+
}
|
|
112
|
+
}
|
|
82
113
|
for (const resource of resourceList) {
|
|
83
114
|
try {
|
|
115
|
+
if (resolvedUserIdentifier !== undefined) {
|
|
116
|
+
tokens[resource] = await client.impersonate({
|
|
117
|
+
userIdentifier: resolvedUserIdentifier,
|
|
118
|
+
resource,
|
|
119
|
+
zoneId: resolvedZoneId,
|
|
120
|
+
});
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
84
123
|
let exchangeRequest;
|
|
85
124
|
if (options.applicationCredential) {
|
|
86
125
|
exchangeRequest = await options.applicationCredential.prepareTokenExchangeRequest(subjectToken, resource, { zoneId: resolvedZoneId });
|
package/dist/cjs/grant.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant.js","sourceRoot":"","sources":["../../src/grant.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"grant.js","sourceRoot":"","sources":["../../src/grant.ts"],"names":[],"mappings":"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"}
|
package/dist/esm/grant.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RequestHandler } from "express";
|
|
1
|
+
import type { Request, RequestHandler } from "express";
|
|
2
2
|
import { AccessContext } from "@keycardai/oauth/server/accessContext";
|
|
3
3
|
import type { ApplicationCredential } from "@keycardai/oauth/credentials";
|
|
4
4
|
import type { AuthenticatedRequest } from "./bearerAuth.js";
|
|
@@ -33,6 +33,22 @@ export interface GrantOptions {
|
|
|
33
33
|
* are selected per request.
|
|
34
34
|
*/
|
|
35
35
|
applicationCredential?: ApplicationCredential;
|
|
36
|
+
/**
|
|
37
|
+
* Resolver for the user identity to impersonate. When set, each
|
|
38
|
+
* per-resource exchange uses the substitute-user impersonation flow
|
|
39
|
+
* (`TokenExchangeClient.impersonate`) with the resolved identifier
|
|
40
|
+
* instead of exchanging the caller's bearer token. The resolver runs
|
|
41
|
+
* once per request and may be async.
|
|
42
|
+
*
|
|
43
|
+
* ```ts
|
|
44
|
+
* grant(resources, {
|
|
45
|
+
* zoneUrl,
|
|
46
|
+
* applicationCredential,
|
|
47
|
+
* userIdentifier: (req) => req.auth.subject,
|
|
48
|
+
* });
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
userIdentifier?: (req: Request) => string | Promise<string>;
|
|
36
52
|
}
|
|
37
53
|
/**
|
|
38
54
|
* Express middleware factory for delegated token exchange (RFC 8693).
|
|
@@ -45,6 +61,12 @@ export interface GrantOptions {
|
|
|
45
61
|
* `TokenResponse` for that resource. On partial failure, some resources
|
|
46
62
|
* may have errors while others succeed.
|
|
47
63
|
*
|
|
64
|
+
* If the request carries no verified bearer token, responds 401 with an
|
|
65
|
+
* RFC 6750 `WWW-Authenticate` challenge; the handler does not run.
|
|
66
|
+
*
|
|
67
|
+
* Multiple `grant()` middlewares may be stacked: each merges its
|
|
68
|
+
* per-resource tokens and errors into the existing `req.accessContext`.
|
|
69
|
+
*
|
|
48
70
|
* ```ts
|
|
49
71
|
* app.use(requireBearerAuth({ issuer: "https://zone.keycard.cloud" }));
|
|
50
72
|
* app.use(grant(["https://graph.microsoft.com"], { zoneUrl: "https://zone.keycard.cloud" }));
|
package/dist/esm/grant.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../src/grant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,
|
|
1
|
+
{"version":3,"file":"grant.d.ts","sourceRoot":"","sources":["../../src/grant.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,OAAO,EAA0B,cAAc,EAAE,MAAM,SAAS,CAAC;AAE/E,OAAO,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AACtE,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,iBAAiB,CAAC;AAC5D,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,qCAAqC,CAAC;AAGvE,MAAM,WAAW,cAAe,SAAQ,oBAAoB;IAC1D,aAAa,EAAE,aAAa,CAAC;CAC9B;AAED,MAAM,WAAW,YAAY;IAC3B;;;OAGG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;;;;;;;;;OAYG;IACH,MAAM,CAAC,EAAE,MAAM,GAAG,CAAC,CAAC,IAAI,EAAE,WAAW,KAAK,MAAM,CAAC,CAAC;IAClD;;;;;OAKG;IACH,qBAAqB,CAAC,EAAE,qBAAqB,CAAC;IAC9C;;;;;;;;;;;;;;OAcG;IACH,cAAc,CAAC,EAAE,CAAC,GAAG,EAAE,OAAO,KAAK,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;CAC7D;AAED;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,wBAAgB,KAAK,CACnB,SAAS,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,EACrC,OAAO,EAAE,YAAY,GACpB,cAAc,CAyIhB"}
|
package/dist/esm/grant.js
CHANGED
|
@@ -12,6 +12,12 @@ import { OAuthError, AuthProviderConfigurationError } from "@keycardai/oauth/err
|
|
|
12
12
|
* `TokenResponse` for that resource. On partial failure, some resources
|
|
13
13
|
* may have errors while others succeed.
|
|
14
14
|
*
|
|
15
|
+
* If the request carries no verified bearer token, responds 401 with an
|
|
16
|
+
* RFC 6750 `WWW-Authenticate` challenge; the handler does not run.
|
|
17
|
+
*
|
|
18
|
+
* Multiple `grant()` middlewares may be stacked: each merges its
|
|
19
|
+
* per-resource tokens and errors into the existing `req.accessContext`.
|
|
20
|
+
*
|
|
15
21
|
* ```ts
|
|
16
22
|
* app.use(requireBearerAuth({ issuer: "https://zone.keycard.cloud" }));
|
|
17
23
|
* app.use(grant(["https://graph.microsoft.com"], { zoneUrl: "https://zone.keycard.cloud" }));
|
|
@@ -32,17 +38,25 @@ export function grant(resources, options) {
|
|
|
32
38
|
// requests — the TokenExchangeClient already caches the token_endpoint
|
|
33
39
|
// internally after the first discovery call.
|
|
34
40
|
const clientCache = new Map();
|
|
35
|
-
return async (req,
|
|
41
|
+
return async (req, res, next) => {
|
|
36
42
|
const authReq = req;
|
|
37
43
|
const subjectToken = authReq.auth?.token;
|
|
38
|
-
const accessCtx = new AccessContext();
|
|
39
44
|
if (!subjectToken) {
|
|
40
|
-
|
|
41
|
-
|
|
45
|
+
// Unauthenticated request: respond 401 with an RFC 6750 challenge
|
|
46
|
+
// (same shape as requireBearerAuth) without invoking the handler.
|
|
47
|
+
const resourceMetadataUrl = `${req.protocol}://${req.host}/.well-known/oauth-protected-resource`;
|
|
48
|
+
res.set("WWW-Authenticate", `Bearer resource_metadata="${resourceMetadataUrl}"`);
|
|
49
|
+
res.status(401).json({
|
|
50
|
+
error: "invalid_request",
|
|
51
|
+
error_description: "Missing bearer token. Ensure requireBearerAuth() runs before grant().",
|
|
42
52
|
});
|
|
43
|
-
|
|
44
|
-
return next();
|
|
53
|
+
return;
|
|
45
54
|
}
|
|
55
|
+
// Reuse an existing context so stacked grant() middlewares accumulate
|
|
56
|
+
// per-resource tokens and errors instead of replacing earlier results.
|
|
57
|
+
const accessCtx = req.accessContext instanceof AccessContext
|
|
58
|
+
? req.accessContext
|
|
59
|
+
: new AccessContext();
|
|
46
60
|
// Resolve zone at request time — zoneId may be a static string or a
|
|
47
61
|
// function that extracts the zone from the verified access token.
|
|
48
62
|
let resolvedZoneId;
|
|
@@ -76,8 +90,33 @@ export function grant(resources, options) {
|
|
|
76
90
|
? resources
|
|
77
91
|
: [resources];
|
|
78
92
|
const tokens = {};
|
|
93
|
+
// Resolve the impersonation target once per request. A resolver failure
|
|
94
|
+
// is recorded as a global error; the handler still runs and access()
|
|
95
|
+
// surfaces the failure.
|
|
96
|
+
let resolvedUserIdentifier;
|
|
97
|
+
if (options.userIdentifier) {
|
|
98
|
+
try {
|
|
99
|
+
resolvedUserIdentifier = await options.userIdentifier(req);
|
|
100
|
+
}
|
|
101
|
+
catch (e) {
|
|
102
|
+
accessCtx.setError({
|
|
103
|
+
message: "Failed to resolve userIdentifier.",
|
|
104
|
+
rawError: String(e),
|
|
105
|
+
});
|
|
106
|
+
req.accessContext = accessCtx;
|
|
107
|
+
return next();
|
|
108
|
+
}
|
|
109
|
+
}
|
|
79
110
|
for (const resource of resourceList) {
|
|
80
111
|
try {
|
|
112
|
+
if (resolvedUserIdentifier !== undefined) {
|
|
113
|
+
tokens[resource] = await client.impersonate({
|
|
114
|
+
userIdentifier: resolvedUserIdentifier,
|
|
115
|
+
resource,
|
|
116
|
+
zoneId: resolvedZoneId,
|
|
117
|
+
});
|
|
118
|
+
continue;
|
|
119
|
+
}
|
|
81
120
|
let exchangeRequest;
|
|
82
121
|
if (options.applicationCredential) {
|
|
83
122
|
exchangeRequest = await options.applicationCredential.prepareTokenExchangeRequest(subjectToken, resource, { zoneId: resolvedZoneId });
|
package/dist/esm/grant.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"grant.js","sourceRoot":"","sources":["../../src/grant.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AAEtE,OAAO,EAAE,UAAU,EAAE,8BAA8B,EAAE,MAAM,yBAAyB,CAAC;
|
|
1
|
+
{"version":3,"file":"grant.js","sourceRoot":"","sources":["../../src/grant.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,EAAE,aAAa,EAAE,MAAM,uCAAuC,CAAC;AAEtE,OAAO,EAAE,UAAU,EAAE,8BAA8B,EAAE,MAAM,yBAAyB,CAAC;AAsDrF;;;;;;;;;;;;;;;;;;;;;;;;;GAyBG;AACH,MAAM,UAAU,KAAK,CACnB,SAAqC,EACrC,OAAqB;IAErB,0DAA0D;IAC1D,MAAM,aAAa,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,IAAI,OAAO,CAAC,MAAM,CAAC,CAAC;IAC5D,IAAI,CAAC,aAAa,EAAE,CAAC;QACnB,MAAM,IAAI,8BAA8B,CACtC,iDAAiD,CAClD,CAAC;IACJ,CAAC;IAED,kEAAkE;IAClE,wEAAwE;IACxE,uEAAuE;IACvE,6CAA6C;IAC7C,MAAM,WAAW,GAAG,IAAI,GAAG,EAA+B,CAAC;IAE3D,OAAO,KAAK,EAAE,GAAY,EAAE,GAAa,EAAE,IAAkB,EAAE,EAAE;QAC/D,MAAM,OAAO,GAAG,GAA2B,CAAC;QAC5C,MAAM,YAAY,GAAG,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;QAEzC,IAAI,CAAC,YAAY,EAAE,CAAC;YAClB,kEAAkE;YAClE,kEAAkE;YAClE,MAAM,mBAAmB,GAAG,GAAG,GAAG,CAAC,QAAQ,MAAM,GAAG,CAAC,IAAI,uCAAuC,CAAC;YACjG,GAAG,CAAC,GAAG,CAAC,kBAAkB,EAAE,6BAA6B,mBAAmB,GAAG,CAAC,CAAC;YACjF,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,KAAK,EAAE,iBAAiB;gBACxB,iBAAiB,EACf,uEAAuE;aAC1E,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QAED,sEAAsE;QACtE,uEAAuE;QACvE,MAAM,SAAS,GACZ,GAAsB,CAAC,aAAa,YAAY,aAAa;YAC5D,CAAC,CAAE,GAAsB,CAAC,aAAa;YACvC,CAAC,CAAC,IAAI,aAAa,EAAE,CAAC;QAE1B,oEAAoE;QACpE,kEAAkE;QAClE,IAAI,cAAkC,CAAC;QACvC,IAAI,CAAC;YACH,cAAc;gBACZ,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU;oBAClC,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC,OAAO,CAAC,IAAI,CAAC;oBAC9B,CAAC,CAAC,OAAO,CAAC,MAAM,CAAC;QACvB,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,OAAO,IAAI,CAAC,CAAC,CAAC,CAAC;QACjB,CAAC;QAED,MAAM,eAAe,GAAG,OAAO,CAAC,OAAO,IAAI,YAAY,CAAC,cAAc,CAAC,CAAC;QACxE,IAAI,CAAC,eAAe,EAAE,CAAC;YACrB,SAAS,CAAC,QAAQ,CAAC,EAAE,OAAO,EAAE,8CAA8C,EAAE,CAAC,CAAC;YAC/E,GAAsB,CAAC,aAAa,GAAG,SAAS,CAAC;YAClD,OAAO,IAAI,EAAE,CAAC;QAChB,CAAC;QAED,mDAAmD;QACnD,IAAI,MAAM,GAAG,WAAW,CAAC,GAAG,CAAC,eAAe,CAAC,CAAC;QAC9C,IAAI,CAAC,MAAM,EAAE,CAAC;YACZ,+DAA+D;YAC/D,mEAAmE;YACnE,kDAAkD;YAClD,MAAM,GAAG,IAAI,mBAAmB,CAAC,eAAe,EAAE;gBAChD,UAAU,EAAE,OAAO,CAAC,qBAAqB;aAC1C,CAAC,CAAC;YACH,WAAW,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;QAC3C,CAAC;QAED,MAAM,YAAY,GAAG,KAAK,CAAC,OAAO,CAAC,SAAS,CAAC;YAC3C,CAAC,CAAC,SAAS;YACX,CAAC,CAAC,CAAC,SAAmB,CAAC,CAAC;QAC1B,MAAM,MAAM,GAAkC,EAAE,CAAC;QAEjD,wEAAwE;QACxE,qEAAqE;QACrE,wBAAwB;QACxB,IAAI,sBAA0C,CAAC;QAC/C,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;YAC3B,IAAI,CAAC;gBACH,sBAAsB,GAAG,MAAM,OAAO,CAAC,cAAc,CAAC,GAAG,CAAC,CAAC;YAC7D,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,SAAS,CAAC,QAAQ,CAAC;oBACjB,OAAO,EAAE,mCAAmC;oBAC5C,QAAQ,EAAE,MAAM,CAAC,CAAC,CAAC;iBACpB,CAAC,CAAC;gBACF,GAAsB,CAAC,aAAa,GAAG,SAAS,CAAC;gBAClD,OAAO,IAAI,EAAE,CAAC;YAChB,CAAC;QACH,CAAC;QAED,KAAK,MAAM,QAAQ,IAAI,YAAY,EAAE,CAAC;YACpC,IAAI,CAAC;gBACH,IAAI,sBAAsB,KAAK,SAAS,EAAE,CAAC;oBACzC,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,MAAM,CAAC,WAAW,CAAC;wBAC1C,cAAc,EAAE,sBAAsB;wBACtC,QAAQ;wBACR,MAAM,EAAE,cAAc;qBACvB,CAAC,CAAC;oBACH,SAAS;gBACX,CAAC;gBACD,IAAI,eAAe,CAAC;gBACpB,IAAI,OAAO,CAAC,qBAAqB,EAAE,CAAC;oBAClC,eAAe,GAAG,MAAM,OAAO,CAAC,qBAAqB,CAAC,2BAA2B,CAC/E,YAAY,EACZ,QAAQ,EACR,EAAE,MAAM,EAAE,cAAc,EAAE,CAC3B,CAAC;gBACJ,CAAC;qBAAM,CAAC;oBACN,eAAe,GAAG;wBAChB,YAAY;wBACZ,QAAQ;wBACR,gBAAgB,EAAE,+CAAwD;qBAC3E,CAAC;gBACJ,CAAC;gBACD,MAAM,CAAC,QAAQ,CAAC,GAAG,MAAM,MAAM,CAAC,aAAa,CAAC,eAAe,EAAE;oBAC7D,MAAM,EAAE,cAAc;iBACvB,CAAC,CAAC;YACL,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,MAAM,MAAM,GAAgF;oBAC1F,OAAO,EAAE,6BAA6B,QAAQ,EAAE;iBACjD,CAAC;gBACF,IAAI,CAAC,YAAY,UAAU,EAAE,CAAC;oBAC5B,MAAM,CAAC,IAAI,GAAG,CAAC,CAAC,SAAS,CAAC;oBAC1B,MAAM,CAAC,WAAW,GAAG,CAAC,CAAC,OAAO,CAAC;gBACjC,CAAC;qBAAM,CAAC;oBACN,MAAM,CAAC,QAAQ,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC;gBAC9B,CAAC;gBACD,SAAS,CAAC,gBAAgB,CAAC,QAAQ,EAAE,MAAM,CAAC,CAAC;YAC/C,CAAC;QACH,CAAC;QAED,SAAS,CAAC,aAAa,CAAC,MAAM,CAAC,CAAC;QAC/B,GAAsB,CAAC,aAAa,GAAG,SAAS,CAAC;QAClD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC;AAED,SAAS,YAAY,CAAC,MAAe;IACnC,IAAI,CAAC,MAAM;QAAE,OAAO,SAAS,CAAC;IAC9B,OAAO,WAAW,MAAM,gBAAgB,CAAC;AAC3C,CAAC"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@keycardai/express",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "[Preview] Keycard auth middleware for Express: bearer token validation, RFC 6750 challenges, delegated token exchange, and OAuth discovery routes",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|