feat: add logto auth and backend metrics integration

This commit is contained in:
2026-04-22 15:52:12 +08:00
parent b1bb28f5aa
commit c7ae2fbfcb
38 changed files with 4886 additions and 34 deletions
+54
View File
@@ -0,0 +1,54 @@
import LogtoClient from "@logto/chrome-extension";
import { readAuthConfig } from "../../shared/auth-config";
import type { AuthClientLike } from "./types";
export function createLogtoAuthClient(): AuthClientLike {
const config = readAuthConfig();
const client = new LogtoClient({
appId: config.appId,
endpoint: config.logtoEndpoint,
resources: [config.apiResource],
scopes: config.scopes
});
return {
getAccessToken(resource?: string) {
return client.getAccessToken(resource);
},
getIdTokenClaims() {
return client.getIdTokenClaims();
},
isAuthenticated() {
return client.isAuthenticated();
},
signIn() {
return client.signIn(readChromeIdentity().getRedirectURL("/callback"));
},
signOut() {
return client.signOut(readChromeIdentity().getRedirectURL());
}
};
}
function readChromeIdentity(): {
getRedirectURL: (path?: string) => string;
} {
const identity = (
globalThis as typeof globalThis & {
chrome?: {
identity?: {
getRedirectURL?: (path?: string) => string;
};
};
}
).chrome?.identity;
if (typeof identity?.getRedirectURL !== "function") {
throw new Error("chrome.identity.getRedirectURL is unavailable");
}
return {
getRedirectURL: identity.getRedirectURL.bind(identity)
};
}
+39
View File
@@ -0,0 +1,39 @@
import { readAuthConfig, type AuthConfig } from "../../shared/auth-config";
import { createLoggedInAuthState, createLoggedOutAuthState } from "./state";
import type { AuthClientLike } from "./types";
export interface AuthController {
getAccessToken(): Promise<string>;
getAuthState(): Promise<ReturnType<typeof createLoggedOutAuthState>>;
signIn(): Promise<void>;
signOut(): Promise<void>;
}
export function createAuthController(options: {
authClient: AuthClientLike;
config?: AuthConfig;
}): AuthController {
const config = options.config ?? readAuthConfig();
return {
async getAccessToken() {
return options.authClient.getAccessToken(config.apiResource);
},
async getAuthState() {
const isAuthenticated = await options.authClient.isAuthenticated();
if (!isAuthenticated) {
return createLoggedOutAuthState(config);
}
const claims = await options.authClient.getIdTokenClaims();
return createLoggedInAuthState(claims, config);
},
async signIn() {
await options.authClient.signIn();
},
async signOut() {
await options.authClient.signOut();
}
};
}
+38
View File
@@ -0,0 +1,38 @@
import type { AuthConfig } from "../../shared/auth-config";
import type { AuthStateValue } from "../../shared/auth-messages";
export function createLoggedOutAuthState(
config?: Pick<AuthConfig, "apiResource">
): AuthStateValue {
return {
isAuthenticated: false,
resource: config?.apiResource ?? null
};
}
export function createLoggedInAuthState(
claims: Record<string, unknown> | null | undefined,
config?: Pick<AuthConfig, "apiResource" | "scopes">
): AuthStateValue {
return {
accessTokenExpiresAt: null,
isAuthenticated: true,
resource: config?.apiResource ?? null,
scopes: config?.scopes ?? [],
tokenAvailable: true,
userInfo: {
email: readStringClaim(claims, "email"),
name: readStringClaim(claims, "name"),
sub: readStringClaim(claims, "sub"),
username: readStringClaim(claims, "username")
}
};
}
function readStringClaim(
claims: Record<string, unknown> | null | undefined,
key: string
): string | undefined {
const value = claims?.[key];
return typeof value === "string" ? value : undefined;
}
+7
View File
@@ -0,0 +1,7 @@
export interface AuthClientLike {
getAccessToken(resource?: string): Promise<string>;
getIdTokenClaims(): Promise<Record<string, unknown> | null>;
isAuthenticated(): Promise<boolean>;
signIn(): Promise<void>;
signOut(): Promise<void>;
}