@enactprotocol/trust 2.0.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.
@@ -0,0 +1,89 @@
1
+ /**
2
+ * OAuth Client
3
+ *
4
+ * Wrapper around openid-client for PKCE-based OAuth flow with Sigstore.
5
+ */
6
+
7
+ import { type BaseClient, Issuer, generators } from "openid-client";
8
+
9
+ interface OAuthClientOptions {
10
+ issuer: string;
11
+ redirectURL: string;
12
+ clientID: string;
13
+ clientSecret: string | undefined;
14
+ }
15
+
16
+ /**
17
+ * Initialize an OAuth client by discovering the issuer's configuration
18
+ */
19
+ export async function initializeOAuthClient(options: OAuthClientOptions): Promise<OAuthClient> {
20
+ const issuer = await Issuer.discover(options.issuer);
21
+
22
+ const client = new issuer.Client(
23
+ options.clientSecret
24
+ ? {
25
+ client_id: options.clientID,
26
+ client_secret: options.clientSecret,
27
+ token_endpoint_auth_method: "client_secret_basic" as const,
28
+ }
29
+ : {
30
+ client_id: options.clientID,
31
+ token_endpoint_auth_method: "none" as const,
32
+ }
33
+ );
34
+
35
+ return new OAuthClient(client, options.redirectURL);
36
+ }
37
+
38
+ /**
39
+ * OAuthClient wraps an openid-client Client instance to maintain
40
+ * state for the PKCE authorization flow.
41
+ */
42
+ export class OAuthClient {
43
+ private client: BaseClient;
44
+ private redirectURL: string;
45
+ private verifier: string;
46
+ private nonce: string;
47
+ private state: string;
48
+
49
+ constructor(client: BaseClient, redirectURL: string) {
50
+ this.client = client;
51
+ this.redirectURL = redirectURL;
52
+ this.verifier = generators.codeVerifier(32);
53
+ this.nonce = generators.nonce(32);
54
+ this.state = generators.state(16);
55
+ }
56
+
57
+ /**
58
+ * Get the authorization URL to redirect the user to
59
+ */
60
+ get authorizationUrl(): string {
61
+ return this.client.authorizationUrl({
62
+ scope: "openid email",
63
+ redirect_uri: this.redirectURL,
64
+ code_challenge: generators.codeChallenge(this.verifier),
65
+ code_challenge_method: "S256",
66
+ state: this.state,
67
+ nonce: this.nonce,
68
+ });
69
+ }
70
+
71
+ /**
72
+ * Exchange the callback URL for an ID token
73
+ */
74
+ public async getIDToken(callbackURL: string): Promise<string> {
75
+ const params = this.client.callbackParams(callbackURL);
76
+ const tokenSet = await this.client.callback(this.redirectURL, params, {
77
+ response_type: "code",
78
+ code_verifier: this.verifier,
79
+ state: this.state,
80
+ nonce: this.nonce,
81
+ });
82
+
83
+ if (!tokenSet.id_token) {
84
+ throw new Error("No ID token received from OAuth provider");
85
+ }
86
+
87
+ return tokenSet.id_token;
88
+ }
89
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * OAuth Identity Provider
3
+ *
4
+ * Provides interactive OIDC authentication for keyless signing.
5
+ * Opens a browser for the user to authenticate with their identity provider
6
+ * (GitHub, Google, Microsoft) and returns an OIDC token that can be used
7
+ * with Fulcio to obtain a signing certificate.
8
+ */
9
+
10
+ import open from "open";
11
+ import { initializeOAuthClient } from "./client";
12
+ import { CallbackServer } from "./server";
13
+
14
+ /** Default Sigstore OAuth issuer */
15
+ export const SIGSTORE_OAUTH_ISSUER = "https://oauth2.sigstore.dev/auth";
16
+
17
+ /** Default Sigstore OAuth client ID */
18
+ export const SIGSTORE_CLIENT_ID = "sigstore";
19
+
20
+ export interface OAuthIdentityProviderOptions {
21
+ /** OIDC issuer URL (default: Sigstore public instance) */
22
+ issuer?: string;
23
+ /** OAuth client ID (default: "sigstore") */
24
+ clientID?: string;
25
+ /** OAuth client secret (optional, not needed for public clients) */
26
+ clientSecret?: string;
27
+ /** Redirect URL (optional, auto-generated if not provided) */
28
+ redirectURL?: string;
29
+ }
30
+
31
+ /**
32
+ * IdentityProvider interface - matches sigstore's expected interface
33
+ */
34
+ export interface IdentityProvider {
35
+ getToken: () => Promise<string>;
36
+ }
37
+
38
+ /**
39
+ * OAuthIdentityProvider implements interactive browser-based OAuth flow
40
+ * to obtain an OIDC token for keyless signing.
41
+ */
42
+ export class OAuthIdentityProvider implements IdentityProvider {
43
+ private server: CallbackServer;
44
+ private issuer: string;
45
+ private clientID: string;
46
+ private clientSecret: string | undefined;
47
+
48
+ constructor(options: OAuthIdentityProviderOptions = {}) {
49
+ this.issuer = options.issuer ?? SIGSTORE_OAUTH_ISSUER;
50
+ this.clientID = options.clientID ?? SIGSTORE_CLIENT_ID;
51
+ this.clientSecret = options.clientSecret;
52
+
53
+ let serverOpts: { hostname: string; port: number };
54
+ if (options.redirectURL) {
55
+ const url = new URL(options.redirectURL);
56
+ serverOpts = { hostname: url.hostname, port: Number(url.port) };
57
+ } else {
58
+ // Use random port on localhost
59
+ serverOpts = { hostname: "localhost", port: 0 };
60
+ }
61
+
62
+ this.server = new CallbackServer(serverOpts);
63
+ }
64
+
65
+ /**
66
+ * Get an OIDC token by performing interactive OAuth flow.
67
+ * Opens a browser for the user to authenticate.
68
+ */
69
+ public async getToken(): Promise<string> {
70
+ // Start server to receive OAuth callback
71
+ const serverURL = await this.server.start();
72
+
73
+ // Initialize OAuth client with discovered configuration
74
+ const client = await initializeOAuthClient({
75
+ issuer: this.issuer,
76
+ redirectURL: serverURL,
77
+ clientID: this.clientID,
78
+ clientSecret: this.clientSecret,
79
+ });
80
+
81
+ // Open browser to OAuth login page
82
+ await open(client.authorizationUrl);
83
+
84
+ if (!this.server.callback) {
85
+ throw new Error("callback server not started");
86
+ }
87
+
88
+ // Wait for callback and exchange auth code for ID token
89
+ return this.server.callback.then((callbackURL) => client.getIDToken(callbackURL));
90
+ }
91
+ }
92
+
93
+ // Re-export for convenience
94
+ export { CallbackServer } from "./server";
95
+ export { OAuthClient, initializeOAuthClient } from "./client";
@@ -0,0 +1,163 @@
1
+ /**
2
+ * OAuth Callback Server
3
+ *
4
+ * A simple HTTP server that receives the OAuth redirect callback
5
+ * after the user authenticates in their browser.
6
+ */
7
+
8
+ import http from "node:http";
9
+ import type { Socket } from "node:net";
10
+
11
+ interface CallbackServerOptions {
12
+ port: number;
13
+ hostname: string;
14
+ }
15
+
16
+ /**
17
+ * CallbackServer is a simple HTTP server which receives the OAuth
18
+ * redirect from the OAuth provider after the user signs-in. It will shutdown
19
+ * once the callback is received and the callback promise will resolve with
20
+ * the URL of the incoming request.
21
+ */
22
+ export class CallbackServer {
23
+ private server: http.Server;
24
+ private sockets: Set<Socket>;
25
+ private port: number;
26
+ private hostname: string;
27
+
28
+ public callback: Promise<string> | undefined;
29
+
30
+ constructor(options: CallbackServerOptions) {
31
+ this.server = http.createServer();
32
+ this.sockets = new Set<Socket>();
33
+ this.port = options.port;
34
+ this.hostname = options.hostname;
35
+ }
36
+
37
+ async start(): Promise<string> {
38
+ await new Promise<void>((resolve) => {
39
+ this.server.listen(this.port, this.hostname, resolve);
40
+ });
41
+
42
+ // Keep track of connections so we can force a shutdown
43
+ this.server.on("connection", (socket) => {
44
+ this.sockets.add(socket);
45
+ socket.on("close", () => {
46
+ this.sockets.delete(socket);
47
+ });
48
+ });
49
+
50
+ // The callback will resolve with the incoming request URL
51
+ this.callback = new Promise<string>((resolve) => {
52
+ this.server.on("request", ({ url }, res) => {
53
+ res.writeHead(200, { "Content-Type": "text/html" });
54
+ res.end(AUTH_SUCCESS_HTML);
55
+
56
+ // Shutdown the server and resolve the callback promise
57
+ this.shutdown().then(() => resolve(url!));
58
+ });
59
+ });
60
+
61
+ // Calculate and return the URL which can be used to reach the server
62
+ return this.serverURL(this.server);
63
+ }
64
+
65
+ public async shutdown(): Promise<void> {
66
+ // Destroy all sockets and close the server
67
+ return new Promise<void>((resolve) => {
68
+ for (const socket of this.sockets) {
69
+ socket.destroy();
70
+ this.sockets.delete(socket);
71
+ }
72
+ this.server.close(() => resolve());
73
+ });
74
+ }
75
+
76
+ private serverURL(server: http.Server): string {
77
+ const address = server.address();
78
+ if (address === null) {
79
+ throw new Error("invalid server config: address is null");
80
+ }
81
+ if (typeof address === "string") {
82
+ throw new Error("invalid server config: address is a string");
83
+ }
84
+
85
+ return `http://${this.hostname}:${address.port}`;
86
+ }
87
+ }
88
+
89
+ // Success HTML page shown after authentication
90
+ const AUTH_SUCCESS_HTML = `
91
+ <!DOCTYPE html>
92
+ <html>
93
+ <head>
94
+ <title>Enact - Authentication Successful</title>
95
+ <style>
96
+ :root { font-family: system-ui, -apple-system, sans-serif; }
97
+ body {
98
+ display: flex;
99
+ justify-content: center;
100
+ align-items: center;
101
+ min-height: 100vh;
102
+ margin: 0;
103
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
104
+ }
105
+ .container {
106
+ background: white;
107
+ padding: 3rem;
108
+ border-radius: 1rem;
109
+ box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1);
110
+ text-align: center;
111
+ max-width: 400px;
112
+ }
113
+ .checkmark {
114
+ width: 80px;
115
+ height: 80px;
116
+ margin: 0 auto 1.5rem;
117
+ background: #10b981;
118
+ border-radius: 50%;
119
+ display: flex;
120
+ align-items: center;
121
+ justify-content: center;
122
+ }
123
+ .checkmark svg {
124
+ width: 40px;
125
+ height: 40px;
126
+ stroke: white;
127
+ stroke-width: 3;
128
+ fill: none;
129
+ }
130
+ h1 {
131
+ color: #1f2937;
132
+ margin: 0 0 0.5rem;
133
+ font-size: 1.5rem;
134
+ }
135
+ p {
136
+ color: #6b7280;
137
+ margin: 0;
138
+ font-size: 1rem;
139
+ }
140
+ .brand {
141
+ margin-top: 2rem;
142
+ color: #9ca3af;
143
+ font-size: 0.875rem;
144
+ }
145
+ .brand strong {
146
+ color: #667eea;
147
+ }
148
+ </style>
149
+ </head>
150
+ <body>
151
+ <div class="container">
152
+ <div class="checkmark">
153
+ <svg viewBox="0 0 24 24">
154
+ <polyline points="20 6 9 17 4 12"></polyline>
155
+ </svg>
156
+ </div>
157
+ <h1>Authentication Successful!</h1>
158
+ <p>You may now close this window and return to your terminal.</p>
159
+ <p class="brand">Signed with <strong>Sigstore</strong> via <strong>Enact</strong></p>
160
+ </div>
161
+ </body>
162
+ </html>
163
+ `;