@flytedesk/app-kit 0.0.1 → 0.1.1
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/auth/guards.d.ts +5 -0
- package/dist/auth/guards.js +28 -0
- package/dist/auth/guards.js.map +1 -0
- package/dist/auth/index.d.ts +33 -5
- package/dist/auth/index.js +31 -5
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/oidc-client.d.ts +47 -0
- package/dist/auth/oidc-client.js +162 -0
- package/dist/auth/oidc-client.js.map +1 -0
- package/dist/auth/pkce.d.ts +8 -0
- package/dist/auth/pkce.js +12 -0
- package/dist/auth/pkce.js.map +1 -0
- package/dist/auth/plugin.d.ts +8 -0
- package/dist/auth/plugin.js +362 -0
- package/dist/auth/plugin.js.map +1 -0
- package/dist/auth/tokens.d.ts +24 -0
- package/dist/auth/tokens.js +35 -0
- package/dist/auth/tokens.js.map +1 -0
- package/dist/auth/types.d.ts +156 -0
- package/dist/auth/types.js +2 -0
- package/dist/auth/types.js.map +1 -0
- package/dist/cli/trace-sync.d.ts +2 -0
- package/dist/cli/trace-sync.js +362 -0
- package/dist/cli/trace-sync.js.map +1 -0
- package/dist/rate-limit/index.d.ts +6 -0
- package/dist/rate-limit/index.js +7 -0
- package/dist/rate-limit/index.js.map +1 -0
- package/dist/rate-limit/plugin.d.ts +23 -0
- package/dist/rate-limit/plugin.js +75 -0
- package/dist/rate-limit/plugin.js.map +1 -0
- package/dist/rate-limit/store.d.ts +51 -0
- package/dist/rate-limit/store.js +59 -0
- package/dist/rate-limit/store.js.map +1 -0
- package/dist/trace/client.d.ts +20 -0
- package/dist/trace/client.js +21 -0
- package/dist/trace/client.js.map +1 -0
- package/dist/trace/errors.d.ts +31 -0
- package/dist/trace/errors.js +37 -0
- package/dist/trace/errors.js.map +1 -0
- package/dist/trace/index.d.ts +15 -4
- package/dist/trace/index.js +3 -6
- package/dist/trace/index.js.map +1 -1
- package/dist/trace/queries.d.ts +59 -0
- package/dist/trace/queries.js +147 -0
- package/dist/trace/queries.js.map +1 -0
- package/dist/trace/types.d.ts +72 -0
- package/dist/trace/types.js +14 -0
- package/dist/trace/types.js.map +1 -0
- package/package.json +54 -8
- package/prisma/fragments/trace.meta.json +4 -0
- package/prisma/fragments/trace.prisma +87 -0
- package/prisma/migrations/0001_app_kit_trace_init/migration.sql +91 -0
- package/prisma/migrations/manifest.json +8 -0
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import type { FastifyReply, FastifyRequest } from "fastify";
|
|
2
|
+
export declare function requireAuth(request: FastifyRequest, reply: FastifyReply, done: () => void): void;
|
|
3
|
+
/** Every route re-checks permissions server-side. 403s name the missing permission(s)
|
|
4
|
+
* in `detail`, so a caller (or its UI) can tell exactly what was missing. */
|
|
5
|
+
export declare function requirePermission(...permission: string[]): (request: FastifyRequest, reply: FastifyReply, done: () => void) => void;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export function requireAuth(request, reply, done) {
|
|
2
|
+
if (!request.authUser) {
|
|
3
|
+
reply.code(401).send({ error: "Authentication required", code: "unauthenticated" });
|
|
4
|
+
return;
|
|
5
|
+
}
|
|
6
|
+
done();
|
|
7
|
+
}
|
|
8
|
+
/** Every route re-checks permissions server-side. 403s name the missing permission(s)
|
|
9
|
+
* in `detail`, so a caller (or its UI) can tell exactly what was missing. */
|
|
10
|
+
export function requirePermission(...permission) {
|
|
11
|
+
return function permissionPreHandler(request, reply, done) {
|
|
12
|
+
if (!request.authUser) {
|
|
13
|
+
reply.code(401).send({ error: "Authentication required", code: "unauthenticated" });
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
const missing = permission.filter((p) => !request.authUser.permissions.includes(p));
|
|
17
|
+
if (missing.length) {
|
|
18
|
+
reply.code(403).send({
|
|
19
|
+
error: "Missing permission",
|
|
20
|
+
code: "forbidden",
|
|
21
|
+
detail: missing.join(", "),
|
|
22
|
+
});
|
|
23
|
+
return;
|
|
24
|
+
}
|
|
25
|
+
done();
|
|
26
|
+
};
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=guards.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"guards.js","sourceRoot":"","sources":["../../src/auth/guards.ts"],"names":[],"mappings":"AAEA,MAAM,UAAU,WAAW,CAAC,OAAuB,EAAE,KAAmB,EAAE,IAAgB;IACxF,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;QACtB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;QACpF,OAAO;IACT,CAAC;IACD,IAAI,EAAE,CAAC;AACT,CAAC;AAED;8EAC8E;AAC9E,MAAM,UAAU,iBAAiB,CAAC,GAAG,UAAoB;IACvD,OAAO,SAAS,oBAAoB,CAClC,OAAuB,EACvB,KAAmB,EACnB,IAAgB;QAEhB,IAAI,CAAC,OAAO,CAAC,QAAQ,EAAE,CAAC;YACtB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,yBAAyB,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC,CAAC;YACpF,OAAO;QACT,CAAC;QACD,MAAM,OAAO,GAAG,UAAU,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,CAAC,QAAS,CAAC,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC;QACrF,IAAI,OAAO,CAAC,MAAM,EAAE,CAAC;YACnB,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC;gBACnB,KAAK,EAAE,oBAAoB;gBAC3B,IAAI,EAAE,WAAW;gBACjB,MAAM,EAAE,OAAO,CAAC,IAAI,CAAC,IAAI,CAAC;aAC3B,CAAC,CAAC;YACH,OAAO;QACT,CAAC;QACD,IAAI,EAAE,CAAC;IACT,CAAC,CAAC;AACJ,CAAC"}
|
package/dist/auth/index.d.ts
CHANGED
|
@@ -1,7 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* flytedesk-id BFF/OIDC client —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* flytedesk-id BFF/OIDC client (DEC-38) — a generic, reusable Fastify 5 plugin
|
|
3
|
+
* implementing Authorization Code + PKCE against flytedesk-id, the identity provider
|
|
4
|
+
* shared by every flytedesk app. Generalizes media-planner's apps/api
|
|
5
|
+
* src/{routes/auth.ts,lib/flytedeskId.ts,plugins/auth.ts} so client id/secret, issuer,
|
|
6
|
+
* redirect URI, and app id are config instead of hardcoded to one app.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
*
|
|
10
|
+
* import Fastify from "fastify";
|
|
11
|
+
* import { flytedeskAuth, requireAuth, requirePermission } from "@flytedesk/app-kit/auth";
|
|
12
|
+
*
|
|
13
|
+
* const app = Fastify();
|
|
14
|
+
* await app.register(flytedeskAuth, {
|
|
15
|
+
* clientId: "my-app",
|
|
16
|
+
* clientSecret: process.env.FLYTEDESK_ID_CLIENT_SECRET!,
|
|
17
|
+
* issuer: "https://id.flytedesk.com",
|
|
18
|
+
* redirectUri: "https://my-app.flytedesk.com/api/v1/auth/callback",
|
|
19
|
+
* appId: "my-app",
|
|
20
|
+
* postLoginRedirect: "https://my-app.flytedesk.com/",
|
|
21
|
+
* cookieSecret: process.env.COOKIE_SECRET!,
|
|
22
|
+
* secureCookies: process.env.NODE_ENV === "production",
|
|
23
|
+
* accessTokenSecret: process.env.JWT_SECRET!,
|
|
24
|
+
* store: myPrismaBackedAuthStore,
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* app.get("/me", { preHandler: requireAuth }, async (request) => request.authUser);
|
|
6
28
|
*/
|
|
7
|
-
export
|
|
29
|
+
export { flytedeskAuth } from "./plugin.js";
|
|
30
|
+
export { requireAuth, requirePermission } from "./guards.js";
|
|
31
|
+
export type { AuthedUser, AuthStore, CreateIdpSessionInput, CreateRefreshTokenInput, FlytedeskAuthOptions, IdpRole, IdpSessionRow, RefreshTokenRow, UpdateIdpSessionInput, UserinfoClaims, } from "./types.js";
|
|
32
|
+
export type { IdpAuthorization, IdpTokenResponse, IdTokenClaims, OidcClient } from "./oidc-client.js";
|
|
33
|
+
export { createOidcClient } from "./oidc-client.js";
|
|
34
|
+
export { generatePkce, randomState, type PkcePair } from "./pkce.js";
|
|
35
|
+
export { generateRefreshToken, hashRefreshToken, signAccessToken, verifyAccessToken, type AccessTokenClaims, } from "./tokens.js";
|
package/dist/auth/index.js
CHANGED
|
@@ -1,8 +1,34 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* flytedesk-id BFF/OIDC client —
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
2
|
+
* flytedesk-id BFF/OIDC client (DEC-38) — a generic, reusable Fastify 5 plugin
|
|
3
|
+
* implementing Authorization Code + PKCE against flytedesk-id, the identity provider
|
|
4
|
+
* shared by every flytedesk app. Generalizes media-planner's apps/api
|
|
5
|
+
* src/{routes/auth.ts,lib/flytedeskId.ts,plugins/auth.ts} so client id/secret, issuer,
|
|
6
|
+
* redirect URI, and app id are config instead of hardcoded to one app.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
*
|
|
10
|
+
* import Fastify from "fastify";
|
|
11
|
+
* import { flytedeskAuth, requireAuth, requirePermission } from "@flytedesk/app-kit/auth";
|
|
12
|
+
*
|
|
13
|
+
* const app = Fastify();
|
|
14
|
+
* await app.register(flytedeskAuth, {
|
|
15
|
+
* clientId: "my-app",
|
|
16
|
+
* clientSecret: process.env.FLYTEDESK_ID_CLIENT_SECRET!,
|
|
17
|
+
* issuer: "https://id.flytedesk.com",
|
|
18
|
+
* redirectUri: "https://my-app.flytedesk.com/api/v1/auth/callback",
|
|
19
|
+
* appId: "my-app",
|
|
20
|
+
* postLoginRedirect: "https://my-app.flytedesk.com/",
|
|
21
|
+
* cookieSecret: process.env.COOKIE_SECRET!,
|
|
22
|
+
* secureCookies: process.env.NODE_ENV === "production",
|
|
23
|
+
* accessTokenSecret: process.env.JWT_SECRET!,
|
|
24
|
+
* store: myPrismaBackedAuthStore,
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* app.get("/me", { preHandler: requireAuth }, async (request) => request.authUser);
|
|
6
28
|
*/
|
|
7
|
-
export
|
|
29
|
+
export { flytedeskAuth } from "./plugin.js";
|
|
30
|
+
export { requireAuth, requirePermission } from "./guards.js";
|
|
31
|
+
export { createOidcClient } from "./oidc-client.js";
|
|
32
|
+
export { generatePkce, randomState } from "./pkce.js";
|
|
33
|
+
export { generateRefreshToken, hashRefreshToken, signAccessToken, verifyAccessToken, } from "./tokens.js";
|
|
8
34
|
//# sourceMappingURL=index.js.map
|
package/dist/auth/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/auth/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,OAAO,EAAE,aAAa,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAgB7D,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAEpD,OAAO,EAAE,YAAY,EAAE,WAAW,EAAiB,MAAM,WAAW,CAAC;AAErE,OAAO,EACL,oBAAoB,EACpB,gBAAgB,EAChB,eAAe,EACf,iBAAiB,GAElB,MAAM,aAAa,CAAC"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import type { UserinfoClaims } from "./types.js";
|
|
2
|
+
export interface OidcClientOptions {
|
|
3
|
+
issuer: string;
|
|
4
|
+
clientId: string;
|
|
5
|
+
clientSecret: string;
|
|
6
|
+
redirectUri: string;
|
|
7
|
+
}
|
|
8
|
+
export interface IdpTokenResponse {
|
|
9
|
+
access_token: string;
|
|
10
|
+
id_token: string;
|
|
11
|
+
refresh_token?: string;
|
|
12
|
+
token_type: string;
|
|
13
|
+
expires_in: number;
|
|
14
|
+
}
|
|
15
|
+
export interface IdTokenClaims {
|
|
16
|
+
sub: string;
|
|
17
|
+
}
|
|
18
|
+
export interface IdpAuthorization {
|
|
19
|
+
userId: string;
|
|
20
|
+
appId: string;
|
|
21
|
+
roles: Array<{
|
|
22
|
+
id: string;
|
|
23
|
+
key: string;
|
|
24
|
+
name: string;
|
|
25
|
+
}>;
|
|
26
|
+
permissions: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface OidcClient {
|
|
29
|
+
buildAuthorizeUrl(opts: {
|
|
30
|
+
state: string;
|
|
31
|
+
challenge: string;
|
|
32
|
+
}): string;
|
|
33
|
+
exchangeCodeForTokens(code: string, verifier: string): Promise<IdpTokenResponse>;
|
|
34
|
+
refreshIdpTokens(refreshToken: string): Promise<IdpTokenResponse>;
|
|
35
|
+
verifyIdToken(idToken: string): Promise<IdTokenClaims>;
|
|
36
|
+
fetchUserinfo(idpAccessToken: string): Promise<UserinfoClaims>;
|
|
37
|
+
fetchAuthorization(idpAccessToken: string, appId: string): Promise<IdpAuthorization>;
|
|
38
|
+
/** Best-effort — never throws. Returns whether flytedesk-id accepted the revocation. */
|
|
39
|
+
revokeIdpToken(token: string): Promise<boolean>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* One instance per FlytedeskAuthOptions (i.e. per plugin registration), not a module
|
|
43
|
+
* singleton — keeps the JWKS/discovery caches below scoped to one issuer/client pair
|
|
44
|
+
* instead of leaking across consumers or test instances that register the plugin
|
|
45
|
+
* against different flytedesk-id deployments.
|
|
46
|
+
*/
|
|
47
|
+
export declare function createOidcClient(opts: OidcClientOptions): OidcClient;
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
import { createRemoteJWKSet, jwtVerify } from "jose";
|
|
2
|
+
/**
|
|
3
|
+
* One instance per FlytedeskAuthOptions (i.e. per plugin registration), not a module
|
|
4
|
+
* singleton — keeps the JWKS/discovery caches below scoped to one issuer/client pair
|
|
5
|
+
* instead of leaking across consumers or test instances that register the plugin
|
|
6
|
+
* against different flytedesk-id deployments.
|
|
7
|
+
*/
|
|
8
|
+
export function createOidcClient(opts) {
|
|
9
|
+
function issuerUrl(path) {
|
|
10
|
+
return new URL(path, opts.issuer).toString();
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Per RFC 6749 Appendix B, client_id and client_secret each have to be
|
|
14
|
+
* application/x-www-form-urlencoded BEFORE being joined with `:` and base64'd — not
|
|
15
|
+
* concatenated raw. oidc-provider's token-auth decoder reverses exactly this
|
|
16
|
+
* encoding; a raw, un-encoded secret containing `+`, `/`, or a control character
|
|
17
|
+
* fails that decode and gets rejected with a generic "not properly encoded" error,
|
|
18
|
+
* not one that names the client_id/client_secret as the actual problem.
|
|
19
|
+
*/
|
|
20
|
+
function basicAuthHeader() {
|
|
21
|
+
const clientId = encodeURIComponent(opts.clientId);
|
|
22
|
+
const clientSecret = encodeURIComponent(opts.clientSecret);
|
|
23
|
+
return "Basic " + Buffer.from(`${clientId}:${clientSecret}`).toString("base64");
|
|
24
|
+
}
|
|
25
|
+
async function tokenRequest(body) {
|
|
26
|
+
const res = await fetch(issuerUrl("/token"), {
|
|
27
|
+
method: "POST",
|
|
28
|
+
headers: {
|
|
29
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
30
|
+
Authorization: basicAuthHeader(),
|
|
31
|
+
},
|
|
32
|
+
body: new URLSearchParams(body).toString(),
|
|
33
|
+
});
|
|
34
|
+
if (!res.ok) {
|
|
35
|
+
const text = await res.text().catch(() => "");
|
|
36
|
+
throw new Error(`flytedesk-id /token returned ${res.status}: ${text}`);
|
|
37
|
+
}
|
|
38
|
+
return (await res.json());
|
|
39
|
+
}
|
|
40
|
+
let jwks;
|
|
41
|
+
function getJwks() {
|
|
42
|
+
jwks ??= createRemoteJWKSet(new URL(issuerUrl("/jwks")));
|
|
43
|
+
return jwks;
|
|
44
|
+
}
|
|
45
|
+
let discoveryCache;
|
|
46
|
+
function getDiscovery() {
|
|
47
|
+
discoveryCache ??= fetch(issuerUrl("/.well-known/openid-configuration")).then((res) => {
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
throw new Error(`flytedesk-id discovery returned ${res.status}`);
|
|
50
|
+
return res.json();
|
|
51
|
+
});
|
|
52
|
+
return discoveryCache;
|
|
53
|
+
}
|
|
54
|
+
return {
|
|
55
|
+
buildAuthorizeUrl({ state, challenge }) {
|
|
56
|
+
const url = new URL(issuerUrl("/auth"));
|
|
57
|
+
url.searchParams.set("client_id", opts.clientId);
|
|
58
|
+
url.searchParams.set("redirect_uri", opts.redirectUri);
|
|
59
|
+
url.searchParams.set("response_type", "code");
|
|
60
|
+
url.searchParams.set("scope", "openid email profile");
|
|
61
|
+
url.searchParams.set("code_challenge", challenge);
|
|
62
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
63
|
+
url.searchParams.set("state", state);
|
|
64
|
+
return url.toString();
|
|
65
|
+
},
|
|
66
|
+
/** Authorization Code + PKCE exchange — the one moment this app's client_secret is used. */
|
|
67
|
+
exchangeCodeForTokens(code, verifier) {
|
|
68
|
+
return tokenRequest({
|
|
69
|
+
grant_type: "authorization_code",
|
|
70
|
+
code,
|
|
71
|
+
redirect_uri: opts.redirectUri,
|
|
72
|
+
code_verifier: verifier,
|
|
73
|
+
client_id: opts.clientId,
|
|
74
|
+
});
|
|
75
|
+
},
|
|
76
|
+
/** flytedesk-id rotates refresh tokens on every use — the caller must persist the
|
|
77
|
+
* new one returned here. */
|
|
78
|
+
refreshIdpTokens(refreshToken) {
|
|
79
|
+
return tokenRequest({
|
|
80
|
+
grant_type: "refresh_token",
|
|
81
|
+
refresh_token: refreshToken,
|
|
82
|
+
client_id: opts.clientId,
|
|
83
|
+
});
|
|
84
|
+
},
|
|
85
|
+
/**
|
|
86
|
+
* Verifies the id_token's signature against flytedesk-id's own JWKS — confirms WHO
|
|
87
|
+
* signed in (the `sub`) with a local crypto check, no callback needed. Deliberately
|
|
88
|
+
* returns only `sub`: for the Authorization Code flow OIDC core 5.4 puts profile
|
|
89
|
+
* claims (email, name, ...) in the UserInfo response, not the ID Token, so an
|
|
90
|
+
* id_token here legitimately carries only sub/aud/iss/exp/iat even though
|
|
91
|
+
* `email profile` was requested and granted. See fetchUserinfo below for where
|
|
92
|
+
* those actually come from.
|
|
93
|
+
*/
|
|
94
|
+
async verifyIdToken(idToken) {
|
|
95
|
+
const { payload } = await jwtVerify(idToken, getJwks(), {
|
|
96
|
+
issuer: opts.issuer,
|
|
97
|
+
audience: opts.clientId,
|
|
98
|
+
});
|
|
99
|
+
if (typeof payload.sub !== "string") {
|
|
100
|
+
throw new Error("flytedesk-id id_token is missing the required sub claim");
|
|
101
|
+
}
|
|
102
|
+
return { sub: payload.sub };
|
|
103
|
+
},
|
|
104
|
+
/** `GET {issuer}/me` — the profile claims (email/name/email_verified) the id_token
|
|
105
|
+
* deliberately doesn't carry for the Authorization Code flow. */
|
|
106
|
+
async fetchUserinfo(idpAccessToken) {
|
|
107
|
+
const res = await fetch(issuerUrl("/me"), {
|
|
108
|
+
headers: { Authorization: `Bearer ${idpAccessToken}` },
|
|
109
|
+
});
|
|
110
|
+
if (!res.ok) {
|
|
111
|
+
throw new Error(`flytedesk-id userinfo (/me) returned ${res.status}`);
|
|
112
|
+
}
|
|
113
|
+
const body = (await res.json());
|
|
114
|
+
if (typeof body.sub !== "string" || typeof body.email !== "string") {
|
|
115
|
+
throw new Error("flytedesk-id userinfo (/me) is missing required sub/email claims");
|
|
116
|
+
}
|
|
117
|
+
return {
|
|
118
|
+
sub: body.sub,
|
|
119
|
+
email: body.email,
|
|
120
|
+
emailVerified: body.email_verified === true,
|
|
121
|
+
name: typeof body.name === "string" ? body.name : undefined,
|
|
122
|
+
};
|
|
123
|
+
},
|
|
124
|
+
/** `GET {issuer}/apps/{appId}/authorization` — computed fresh from flytedesk-id's
|
|
125
|
+
* database on every call, never cached there. Callers must preserve that property
|
|
126
|
+
* by calling this live rather than memoizing the result — see FlytedeskAuthOptions
|
|
127
|
+
* authorizationCacheTtlMs's doc comment for the one sanctioned exception. */
|
|
128
|
+
async fetchAuthorization(idpAccessToken, appId) {
|
|
129
|
+
const res = await fetch(issuerUrl(`/apps/${appId}/authorization`), {
|
|
130
|
+
headers: { Authorization: `Bearer ${idpAccessToken}` },
|
|
131
|
+
});
|
|
132
|
+
if (!res.ok) {
|
|
133
|
+
throw new Error(`flytedesk-id authorization endpoint returned ${res.status}`);
|
|
134
|
+
}
|
|
135
|
+
return (await res.json());
|
|
136
|
+
},
|
|
137
|
+
/** Best-effort — revokes the IdP-side refresh token (and, transitively, its grant)
|
|
138
|
+
* on logout if flytedesk-id exposes a revocation endpoint. Never throws: a failed
|
|
139
|
+
* revocation attempt must not block local logout, since the local session is what
|
|
140
|
+
* actually keeps a signed-out browser from calling the consuming app's API again. */
|
|
141
|
+
async revokeIdpToken(token) {
|
|
142
|
+
try {
|
|
143
|
+
const discovery = await getDiscovery();
|
|
144
|
+
if (!discovery.revocation_endpoint)
|
|
145
|
+
return false;
|
|
146
|
+
const res = await fetch(discovery.revocation_endpoint, {
|
|
147
|
+
method: "POST",
|
|
148
|
+
headers: {
|
|
149
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
150
|
+
Authorization: basicAuthHeader(),
|
|
151
|
+
},
|
|
152
|
+
body: new URLSearchParams({ token }).toString(),
|
|
153
|
+
});
|
|
154
|
+
return res.ok;
|
|
155
|
+
}
|
|
156
|
+
catch {
|
|
157
|
+
return false;
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
//# sourceMappingURL=oidc-client.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"oidc-client.js","sourceRoot":"","sources":["../../src/auth/oidc-client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,SAAS,EAAwB,MAAM,MAAM,CAAC;AA6C3E;;;;;GAKG;AACH,MAAM,UAAU,gBAAgB,CAAC,IAAuB;IACtD,SAAS,SAAS,CAAC,IAAY;QAC7B,OAAO,IAAI,GAAG,CAAC,IAAI,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC,QAAQ,EAAE,CAAC;IAC/C,CAAC;IAED;;;;;;;OAOG;IACH,SAAS,eAAe;QACtB,MAAM,QAAQ,GAAG,kBAAkB,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAC;QACnD,MAAM,YAAY,GAAG,kBAAkB,CAAC,IAAI,CAAC,YAAY,CAAC,CAAC;QAC3D,OAAO,QAAQ,GAAG,MAAM,CAAC,IAAI,CAAC,GAAG,QAAQ,IAAI,YAAY,EAAE,CAAC,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC;IAClF,CAAC;IAED,KAAK,UAAU,YAAY,CAAC,IAA4B;QACtD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,QAAQ,CAAC,EAAE;YAC3C,MAAM,EAAE,MAAM;YACd,OAAO,EAAE;gBACP,cAAc,EAAE,mCAAmC;gBACnD,aAAa,EAAE,eAAe,EAAE;aACjC;YACD,IAAI,EAAE,IAAI,eAAe,CAAC,IAAI,CAAC,CAAC,QAAQ,EAAE;SAC3C,CAAC,CAAC;QACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;YACZ,MAAM,IAAI,GAAG,MAAM,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,EAAE,CAAC,CAAC;YAC9C,MAAM,IAAI,KAAK,CAAC,gCAAgC,GAAG,CAAC,MAAM,KAAK,IAAI,EAAE,CAAC,CAAC;QACzE,CAAC;QACD,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAqB,CAAC;IAChD,CAAC;IAED,IAAI,IAAiC,CAAC;IACtC,SAAS,OAAO;QACd,IAAI,KAAK,kBAAkB,CAAC,IAAI,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;QACzD,OAAO,IAAI,CAAC;IACd,CAAC;IAED,IAAI,cAAsD,CAAC;IAC3D,SAAS,YAAY;QACnB,cAAc,KAAK,KAAK,CAAC,SAAS,CAAC,mCAAmC,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE,EAAE;YACpF,IAAI,CAAC,GAAG,CAAC,EAAE;gBAAE,MAAM,IAAI,KAAK,CAAC,mCAAmC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YAC9E,OAAO,GAAG,CAAC,IAAI,EAAgC,CAAC;QAClD,CAAC,CAAC,CAAC;QACH,OAAO,cAAc,CAAC;IACxB,CAAC;IAED,OAAO;QACL,iBAAiB,CAAC,EAAE,KAAK,EAAE,SAAS,EAAE;YACpC,MAAM,GAAG,GAAG,IAAI,GAAG,CAAC,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC;YACxC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,WAAW,EAAE,IAAI,CAAC,QAAQ,CAAC,CAAC;YACjD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,cAAc,EAAE,IAAI,CAAC,WAAW,CAAC,CAAC;YACvD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;YAC9C,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,sBAAsB,CAAC,CAAC;YACtD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,gBAAgB,EAAE,SAAS,CAAC,CAAC;YAClD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,uBAAuB,EAAE,MAAM,CAAC,CAAC;YACtD,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC;YACrC,OAAO,GAAG,CAAC,QAAQ,EAAE,CAAC;QACxB,CAAC;QAED,4FAA4F;QAC5F,qBAAqB,CAAC,IAAI,EAAE,QAAQ;YAClC,OAAO,YAAY,CAAC;gBAClB,UAAU,EAAE,oBAAoB;gBAChC,IAAI;gBACJ,YAAY,EAAE,IAAI,CAAC,WAAW;gBAC9B,aAAa,EAAE,QAAQ;gBACvB,SAAS,EAAE,IAAI,CAAC,QAAQ;aACzB,CAAC,CAAC;QACL,CAAC;QAED;qCAC6B;QAC7B,gBAAgB,CAAC,YAAY;YAC3B,OAAO,YAAY,CAAC;gBAClB,UAAU,EAAE,eAAe;gBAC3B,aAAa,EAAE,YAAY;gBAC3B,SAAS,EAAE,IAAI,CAAC,QAAQ;aACzB,CAAC,CAAC;QACL,CAAC;QAED;;;;;;;;WAQG;QACH,KAAK,CAAC,aAAa,CAAC,OAAO;YACzB,MAAM,EAAE,OAAO,EAAE,GAAG,MAAM,SAAS,CAAC,OAAO,EAAE,OAAO,EAAE,EAAE;gBACtD,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,QAAQ,EAAE,IAAI,CAAC,QAAQ;aACxB,CAAC,CAAC;YACH,IAAI,OAAO,OAAO,CAAC,GAAG,KAAK,QAAQ,EAAE,CAAC;gBACpC,MAAM,IAAI,KAAK,CAAC,yDAAyD,CAAC,CAAC;YAC7E,CAAC;YACD,OAAO,EAAE,GAAG,EAAE,OAAO,CAAC,GAAG,EAAE,CAAC;QAC9B,CAAC;QAED;0EACkE;QAClE,KAAK,CAAC,aAAa,CAAC,cAAc;YAChC,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,EAAE;gBACxC,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,cAAc,EAAE,EAAE;aACvD,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,wCAAwC,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YACxE,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAA4B,CAAC;YAC3D,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,EAAE,CAAC;gBACnE,MAAM,IAAI,KAAK,CAAC,kEAAkE,CAAC,CAAC;YACtF,CAAC;YACD,OAAO;gBACL,GAAG,EAAE,IAAI,CAAC,GAAG;gBACb,KAAK,EAAE,IAAI,CAAC,KAAK;gBACjB,aAAa,EAAE,IAAI,CAAC,cAAc,KAAK,IAAI;gBAC3C,IAAI,EAAE,OAAO,IAAI,CAAC,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,SAAS;aAC5D,CAAC;QACJ,CAAC;QAED;;;sFAG8E;QAC9E,KAAK,CAAC,kBAAkB,CAAC,cAAc,EAAE,KAAK;YAC5C,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,SAAS,KAAK,gBAAgB,CAAC,EAAE;gBACjE,OAAO,EAAE,EAAE,aAAa,EAAE,UAAU,cAAc,EAAE,EAAE;aACvD,CAAC,CAAC;YACH,IAAI,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC;gBACZ,MAAM,IAAI,KAAK,CAAC,gDAAgD,GAAG,CAAC,MAAM,EAAE,CAAC,CAAC;YAChF,CAAC;YACD,OAAO,CAAC,MAAM,GAAG,CAAC,IAAI,EAAE,CAAqB,CAAC;QAChD,CAAC;QAED;;;8FAGsF;QACtF,KAAK,CAAC,cAAc,CAAC,KAAK;YACxB,IAAI,CAAC;gBACH,MAAM,SAAS,GAAG,MAAM,YAAY,EAAE,CAAC;gBACvC,IAAI,CAAC,SAAS,CAAC,mBAAmB;oBAAE,OAAO,KAAK,CAAC;gBACjD,MAAM,GAAG,GAAG,MAAM,KAAK,CAAC,SAAS,CAAC,mBAAmB,EAAE;oBACrD,MAAM,EAAE,MAAM;oBACd,OAAO,EAAE;wBACP,cAAc,EAAE,mCAAmC;wBACnD,aAAa,EAAE,eAAe,EAAE;qBACjC;oBACD,IAAI,EAAE,IAAI,eAAe,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,QAAQ,EAAE;iBAChD,CAAC,CAAC;gBACH,OAAO,GAAG,CAAC,EAAE,CAAC;YAChB,CAAC;YAAC,MAAM,CAAC;gBACP,OAAO,KAAK,CAAC;YACf,CAAC;QACH,CAAC;KACF,CAAC;AACJ,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export interface PkcePair {
|
|
2
|
+
verifier: string;
|
|
3
|
+
challenge: string;
|
|
4
|
+
}
|
|
5
|
+
/** PKCE S256, per RFC 7636 — flytedesk-id makes this mandatory for every client, and
|
|
6
|
+
* this module never offers `plain` as an option. */
|
|
7
|
+
export declare function generatePkce(): PkcePair;
|
|
8
|
+
export declare function randomState(): string;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { randomBytes, createHash } from "node:crypto";
|
|
2
|
+
/** PKCE S256, per RFC 7636 — flytedesk-id makes this mandatory for every client, and
|
|
3
|
+
* this module never offers `plain` as an option. */
|
|
4
|
+
export function generatePkce() {
|
|
5
|
+
const verifier = randomBytes(32).toString("base64url"); // 43 chars — within the 43-128 range RFC 7636 requires
|
|
6
|
+
const challenge = createHash("sha256").update(verifier).digest("base64url");
|
|
7
|
+
return { verifier, challenge };
|
|
8
|
+
}
|
|
9
|
+
export function randomState() {
|
|
10
|
+
return randomBytes(24).toString("base64url");
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=pkce.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pkce.js","sourceRoot":"","sources":["../../src/auth/pkce.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,WAAW,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAOtD;qDACqD;AACrD,MAAM,UAAU,YAAY;IAC1B,MAAM,QAAQ,GAAG,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC,CAAC,uDAAuD;IAC/G,MAAM,SAAS,GAAG,UAAU,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,WAAW,CAAC,CAAC;IAC5E,OAAO,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;AACjC,CAAC;AAED,MAAM,UAAU,WAAW;IACzB,OAAO,WAAW,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,CAAC;AAC/C,CAAC"}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { FastifyPluginAsync } from "fastify";
|
|
2
|
+
import type { AuthedUser, FlytedeskAuthOptions } from "./types.js";
|
|
3
|
+
declare module "fastify" {
|
|
4
|
+
interface FastifyRequest {
|
|
5
|
+
authUser?: AuthedUser;
|
|
6
|
+
}
|
|
7
|
+
}
|
|
8
|
+
export declare const flytedeskAuth: FastifyPluginAsync<FlytedeskAuthOptions>;
|