@caido/server-auth 0.1.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/index.cjs +400 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +273 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +273 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +393 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +36 -0
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,400 @@
|
|
|
1
|
+
let _urql_core = require("@urql/core");
|
|
2
|
+
let graphql_ws = require("graphql-ws");
|
|
3
|
+
let graphql_tag = require("graphql-tag");
|
|
4
|
+
|
|
5
|
+
//#region src/errors.ts
|
|
6
|
+
/**
|
|
7
|
+
* Base error class for authentication-related errors.
|
|
8
|
+
*/
|
|
9
|
+
var AuthenticationError = class extends Error {
|
|
10
|
+
constructor(message) {
|
|
11
|
+
super(message);
|
|
12
|
+
this.name = "AuthenticationError";
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Error thrown when the authentication flow fails to start.
|
|
17
|
+
*/
|
|
18
|
+
var AuthenticationFlowError = class extends AuthenticationError {
|
|
19
|
+
/** Error code from the API */
|
|
20
|
+
code;
|
|
21
|
+
constructor(code, message) {
|
|
22
|
+
super(`${code}: ${message}`);
|
|
23
|
+
this.name = "AuthenticationFlowError";
|
|
24
|
+
this.code = code;
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Error thrown when token refresh fails.
|
|
29
|
+
*/
|
|
30
|
+
var TokenRefreshError = class extends AuthenticationError {
|
|
31
|
+
/** Error code from the API */
|
|
32
|
+
code;
|
|
33
|
+
constructor(code, message) {
|
|
34
|
+
super(`${code}: ${message}`);
|
|
35
|
+
this.name = "TokenRefreshError";
|
|
36
|
+
this.code = code;
|
|
37
|
+
}
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Error thrown when device approval fails.
|
|
41
|
+
*/
|
|
42
|
+
var DeviceApprovalError = class extends AuthenticationError {
|
|
43
|
+
/** HTTP status code if available */
|
|
44
|
+
statusCode;
|
|
45
|
+
constructor(message, statusCode) {
|
|
46
|
+
super(message);
|
|
47
|
+
this.name = "DeviceApprovalError";
|
|
48
|
+
this.statusCode = statusCode;
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
/**
|
|
52
|
+
* Error thrown when fetching device information fails.
|
|
53
|
+
*/
|
|
54
|
+
var DeviceInformationError = class extends AuthenticationError {
|
|
55
|
+
/** HTTP status code if available */
|
|
56
|
+
statusCode;
|
|
57
|
+
constructor(message, statusCode) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = "DeviceInformationError";
|
|
60
|
+
this.statusCode = statusCode;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/queries.ts
|
|
66
|
+
const START_AUTHENTICATION_FLOW = graphql_tag.gql`
|
|
67
|
+
mutation StartAuthenticationFlow {
|
|
68
|
+
startAuthenticationFlow {
|
|
69
|
+
request {
|
|
70
|
+
id
|
|
71
|
+
userCode
|
|
72
|
+
verificationUrl
|
|
73
|
+
expiresAt
|
|
74
|
+
}
|
|
75
|
+
error {
|
|
76
|
+
code
|
|
77
|
+
message
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
`;
|
|
82
|
+
const CREATED_AUTHENTICATION_TOKEN = graphql_tag.gql`
|
|
83
|
+
subscription CreatedAuthenticationToken($requestId: ID!) {
|
|
84
|
+
createdAuthenticationToken(requestId: $requestId) {
|
|
85
|
+
token {
|
|
86
|
+
accessToken
|
|
87
|
+
refreshToken
|
|
88
|
+
expiresAt
|
|
89
|
+
}
|
|
90
|
+
error {
|
|
91
|
+
code
|
|
92
|
+
message
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
`;
|
|
97
|
+
const REFRESH_AUTHENTICATION_TOKEN = graphql_tag.gql`
|
|
98
|
+
mutation RefreshAuthenticationToken($refreshToken: String!) {
|
|
99
|
+
refreshAuthenticationToken(refreshToken: $refreshToken) {
|
|
100
|
+
token {
|
|
101
|
+
accessToken
|
|
102
|
+
refreshToken
|
|
103
|
+
expiresAt
|
|
104
|
+
}
|
|
105
|
+
error {
|
|
106
|
+
code
|
|
107
|
+
message
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
`;
|
|
112
|
+
|
|
113
|
+
//#endregion
|
|
114
|
+
//#region src/client.ts
|
|
115
|
+
/**
|
|
116
|
+
* Client for authenticating with a Caido instance.
|
|
117
|
+
*
|
|
118
|
+
* @example
|
|
119
|
+
* ```typescript
|
|
120
|
+
* import { CaidoAuth, BrowserApprover } from "@caido/auth";
|
|
121
|
+
*
|
|
122
|
+
* const auth = new CaidoAuth(
|
|
123
|
+
* "http://localhost:8080",
|
|
124
|
+
* new BrowserApprover((request) => {
|
|
125
|
+
* console.log(`Visit ${request.verificationUrl}`);
|
|
126
|
+
* })
|
|
127
|
+
* );
|
|
128
|
+
*
|
|
129
|
+
* const token = await auth.startAuthenticationFlow();
|
|
130
|
+
* console.log("Access token:", token.accessToken);
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
var CaidoAuth = class {
|
|
134
|
+
instanceUrl;
|
|
135
|
+
graphqlUrl;
|
|
136
|
+
websocketUrl;
|
|
137
|
+
approver;
|
|
138
|
+
client;
|
|
139
|
+
/**
|
|
140
|
+
* Create a new CaidoAuth client.
|
|
141
|
+
*
|
|
142
|
+
* @param instanceUrl - Base URL of the Caido instance (e.g., "http://localhost:8080")
|
|
143
|
+
* @param approver - The approver to use for the authentication flow
|
|
144
|
+
*/
|
|
145
|
+
constructor(instanceUrl, approver) {
|
|
146
|
+
this.instanceUrl = instanceUrl.replace(/\/$/, "");
|
|
147
|
+
this.graphqlUrl = `${this.instanceUrl}/graphql`;
|
|
148
|
+
this.websocketUrl = this.getWebsocketUrl();
|
|
149
|
+
this.approver = approver;
|
|
150
|
+
this.client = new _urql_core.Client({
|
|
151
|
+
url: this.graphqlUrl,
|
|
152
|
+
exchanges: [_urql_core.fetchExchange]
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Convert HTTP(S) URL to WS(S) URL for subscriptions.
|
|
157
|
+
*/
|
|
158
|
+
getWebsocketUrl() {
|
|
159
|
+
const url = new URL(this.graphqlUrl);
|
|
160
|
+
return `${url.protocol === "https:" ? "wss:" : "ws:"}//${url.host}/ws/graphql`;
|
|
161
|
+
}
|
|
162
|
+
/**
|
|
163
|
+
* Start the device code authentication flow.
|
|
164
|
+
*
|
|
165
|
+
* This method:
|
|
166
|
+
* 1. Initiates the authentication flow via GraphQL mutation
|
|
167
|
+
* 2. Calls the approver with the authentication request
|
|
168
|
+
* 3. Waits for the user to authorize via WebSocket subscription
|
|
169
|
+
* 4. Returns the authentication token once approved
|
|
170
|
+
*
|
|
171
|
+
* @returns The authentication token
|
|
172
|
+
* @throws {AuthenticationFlowError} If the flow fails to start
|
|
173
|
+
* @throws {AuthenticationError} If token retrieval fails
|
|
174
|
+
*/
|
|
175
|
+
async startAuthenticationFlow() {
|
|
176
|
+
const result = await this.client.mutation(START_AUTHENTICATION_FLOW, {}).toPromise();
|
|
177
|
+
if (result.error) throw new AuthenticationFlowError("GRAPHQL_ERROR", result.error.message);
|
|
178
|
+
const payload = result.data?.startAuthenticationFlow;
|
|
179
|
+
if (!payload) throw new AuthenticationFlowError("NO_RESPONSE", "No response from startAuthenticationFlow");
|
|
180
|
+
if (payload.error) throw new AuthenticationFlowError(payload.error.code, payload.error.message);
|
|
181
|
+
if (!payload.request) throw new AuthenticationFlowError("NO_REQUEST", "No authentication request returned");
|
|
182
|
+
const authRequest = {
|
|
183
|
+
id: payload.request.id,
|
|
184
|
+
userCode: payload.request.userCode,
|
|
185
|
+
verificationUrl: payload.request.verificationUrl,
|
|
186
|
+
expiresAt: new Date(payload.request.expiresAt)
|
|
187
|
+
};
|
|
188
|
+
await this.approver.approve(authRequest);
|
|
189
|
+
return await this.waitForToken(authRequest.id);
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Subscribe and wait for the authentication token.
|
|
193
|
+
*
|
|
194
|
+
* @param requestId - The authentication request ID
|
|
195
|
+
* @returns The authentication token once the user authorizes
|
|
196
|
+
* @throws {AuthenticationError} If subscription fails or returns an error
|
|
197
|
+
*/
|
|
198
|
+
async waitForToken(requestId) {
|
|
199
|
+
return new Promise((resolve, reject) => {
|
|
200
|
+
const wsClient = (0, graphql_ws.createClient)({ url: this.websocketUrl });
|
|
201
|
+
const unsubscribe = wsClient.subscribe({
|
|
202
|
+
query: CREATED_AUTHENTICATION_TOKEN.loc?.source.body ?? `subscription CreatedAuthenticationToken($requestId: ID!) {
|
|
203
|
+
createdAuthenticationToken(requestId: $requestId) {
|
|
204
|
+
token { accessToken refreshToken expiresAt }
|
|
205
|
+
error { code message }
|
|
206
|
+
}
|
|
207
|
+
}`,
|
|
208
|
+
variables: { requestId }
|
|
209
|
+
}, {
|
|
210
|
+
next: (result) => {
|
|
211
|
+
const payload = result.data?.createdAuthenticationToken;
|
|
212
|
+
if (payload?.error) {
|
|
213
|
+
unsubscribe();
|
|
214
|
+
wsClient.dispose();
|
|
215
|
+
reject(new AuthenticationError(`${payload.error.code}: ${payload.error.message}`));
|
|
216
|
+
return;
|
|
217
|
+
}
|
|
218
|
+
if (payload?.token) {
|
|
219
|
+
unsubscribe();
|
|
220
|
+
wsClient.dispose();
|
|
221
|
+
resolve({
|
|
222
|
+
accessToken: payload.token.accessToken,
|
|
223
|
+
refreshToken: payload.token.refreshToken,
|
|
224
|
+
expiresAt: new Date(payload.token.expiresAt)
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
},
|
|
228
|
+
error: (error) => {
|
|
229
|
+
wsClient.dispose();
|
|
230
|
+
reject(new AuthenticationError(error instanceof Error ? error.message : String(error)));
|
|
231
|
+
},
|
|
232
|
+
complete: () => {
|
|
233
|
+
wsClient.dispose();
|
|
234
|
+
reject(new AuthenticationError("Subscription ended without receiving token"));
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
/**
|
|
240
|
+
* Refresh an access token using a refresh token.
|
|
241
|
+
*
|
|
242
|
+
* @param refreshToken - The refresh token from a previous authentication
|
|
243
|
+
* @returns New authentication token with updated access and refresh tokens
|
|
244
|
+
* @throws {TokenRefreshError} If the refresh fails
|
|
245
|
+
*/
|
|
246
|
+
async refreshToken(refreshToken) {
|
|
247
|
+
const result = await this.client.mutation(REFRESH_AUTHENTICATION_TOKEN, { refreshToken }).toPromise();
|
|
248
|
+
if (result.error) throw new TokenRefreshError("GRAPHQL_ERROR", result.error.message);
|
|
249
|
+
const payload = result.data?.refreshAuthenticationToken;
|
|
250
|
+
if (!payload) throw new TokenRefreshError("NO_RESPONSE", "No response from refreshAuthenticationToken");
|
|
251
|
+
if (payload.error) throw new TokenRefreshError(payload.error.code, payload.error.message);
|
|
252
|
+
if (!payload.token) throw new TokenRefreshError("NO_TOKEN", "No token returned from refresh");
|
|
253
|
+
return {
|
|
254
|
+
accessToken: payload.token.accessToken,
|
|
255
|
+
refreshToken: payload.token.refreshToken,
|
|
256
|
+
expiresAt: new Date(payload.token.expiresAt)
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
//#endregion
|
|
262
|
+
//#region src/approvers/browser.ts
|
|
263
|
+
/**
|
|
264
|
+
* Browser-based approver that delegates to a callback function.
|
|
265
|
+
* The callback should display the verification URL and user code to the user,
|
|
266
|
+
* who then manually approves the request in their browser.
|
|
267
|
+
*
|
|
268
|
+
* @example
|
|
269
|
+
* ```typescript
|
|
270
|
+
* const approver = new BrowserApprover((request) => {
|
|
271
|
+
* console.log(`Visit ${request.verificationUrl}`);
|
|
272
|
+
* });
|
|
273
|
+
* ```
|
|
274
|
+
*/
|
|
275
|
+
var BrowserApprover = class {
|
|
276
|
+
onRequest;
|
|
277
|
+
/**
|
|
278
|
+
* Create a new BrowserApprover.
|
|
279
|
+
*
|
|
280
|
+
* @param onRequest - Callback function that will be called with the authentication request
|
|
281
|
+
*/
|
|
282
|
+
constructor(onRequest) {
|
|
283
|
+
this.onRequest = onRequest;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Approve the authentication request by calling the callback.
|
|
287
|
+
* The actual approval happens when the user visits the URL and enters the code.
|
|
288
|
+
*
|
|
289
|
+
* @param request - The authentication request
|
|
290
|
+
*/
|
|
291
|
+
async approve(request) {
|
|
292
|
+
await this.onRequest(request);
|
|
293
|
+
}
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
//#endregion
|
|
297
|
+
//#region src/approvers/pat.ts
|
|
298
|
+
const DEFAULT_API_URL = "https://api.caido.io";
|
|
299
|
+
/**
|
|
300
|
+
* PAT-based approver that automatically approves device code requests.
|
|
301
|
+
* Uses a Personal Access Token to call the Caido API directly.
|
|
302
|
+
*
|
|
303
|
+
* @example
|
|
304
|
+
* ```typescript
|
|
305
|
+
* // Approve all scopes
|
|
306
|
+
* const approver = new PATApprover({ pat: "caido_xxxxx" });
|
|
307
|
+
*
|
|
308
|
+
* // Approve only specific scopes
|
|
309
|
+
* const limitedApprover = new PATApprover({
|
|
310
|
+
* pat: "caido_xxxxx",
|
|
311
|
+
* allowedScopes: ["read:projects", "write:requests"],
|
|
312
|
+
* });
|
|
313
|
+
* ```
|
|
314
|
+
*/
|
|
315
|
+
var PATApprover = class {
|
|
316
|
+
pat;
|
|
317
|
+
allowedScopes;
|
|
318
|
+
apiUrl;
|
|
319
|
+
/**
|
|
320
|
+
* Create a new PATApprover.
|
|
321
|
+
*
|
|
322
|
+
* @param options - Configuration options for the approver
|
|
323
|
+
*/
|
|
324
|
+
constructor(options) {
|
|
325
|
+
this.pat = options.pat;
|
|
326
|
+
this.allowedScopes = options.allowedScopes;
|
|
327
|
+
this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\/$/, "");
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Approve the authentication request using the PAT.
|
|
331
|
+
* First fetches device information to get available scopes,
|
|
332
|
+
* then filters scopes if allowedScopes is set,
|
|
333
|
+
* and finally approves the device.
|
|
334
|
+
*
|
|
335
|
+
* @param request - The authentication request
|
|
336
|
+
* @throws {DeviceInformationError} If fetching device information fails
|
|
337
|
+
* @throws {DeviceApprovalError} If approving the device fails
|
|
338
|
+
*/
|
|
339
|
+
async approve(request) {
|
|
340
|
+
let scopesToApprove = (await this.getDeviceInformation(request.userCode)).scopes.map((s) => s.name);
|
|
341
|
+
if (this.allowedScopes) scopesToApprove = scopesToApprove.filter((scope) => this.allowedScopes.includes(scope));
|
|
342
|
+
await this.approveDevice(request.userCode, scopesToApprove);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* Fetch device information from the API.
|
|
346
|
+
*
|
|
347
|
+
* @param userCode - The user code from the authentication request
|
|
348
|
+
* @returns The device information including available scopes
|
|
349
|
+
* @throws {DeviceInformationError} If the request fails
|
|
350
|
+
*/
|
|
351
|
+
async getDeviceInformation(userCode) {
|
|
352
|
+
const params = new URLSearchParams();
|
|
353
|
+
params.append("user_code", userCode);
|
|
354
|
+
const url = new URL(`${this.apiUrl}/oauth2/device/information`);
|
|
355
|
+
url.search = params.toString();
|
|
356
|
+
const response = await fetch(url, {
|
|
357
|
+
method: "GET",
|
|
358
|
+
headers: {
|
|
359
|
+
Authorization: `Bearer ${this.pat}`,
|
|
360
|
+
Accept: "application/json"
|
|
361
|
+
}
|
|
362
|
+
});
|
|
363
|
+
if (!response.ok) throw new DeviceInformationError(`Failed to get device information: ${await response.text().catch(() => "Unknown error")}`, response.status);
|
|
364
|
+
return await response.json();
|
|
365
|
+
}
|
|
366
|
+
/**
|
|
367
|
+
* Approve the device with the specified scopes.
|
|
368
|
+
*
|
|
369
|
+
* @param userCode - The user code from the authentication request
|
|
370
|
+
* @param scopes - The scopes to approve
|
|
371
|
+
* @throws {DeviceApprovalError} If the request fails
|
|
372
|
+
*/
|
|
373
|
+
async approveDevice(userCode, scopes) {
|
|
374
|
+
const params = new URLSearchParams();
|
|
375
|
+
params.append("user_code", userCode);
|
|
376
|
+
params.append("scope", scopes.join(","));
|
|
377
|
+
const url = new URL(`${this.apiUrl}/oauth2/device/approve`);
|
|
378
|
+
url.search = params.toString();
|
|
379
|
+
const response = await fetch(url, {
|
|
380
|
+
method: "POST",
|
|
381
|
+
headers: {
|
|
382
|
+
Authorization: `Bearer ${this.pat}`,
|
|
383
|
+
"Content-Type": "application/json",
|
|
384
|
+
Accept: "application/json"
|
|
385
|
+
}
|
|
386
|
+
});
|
|
387
|
+
if (!response.ok) throw new DeviceApprovalError(`Failed to approve device: ${await response.text().catch(() => "Unknown error")}`, response.status);
|
|
388
|
+
}
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
//#endregion
|
|
392
|
+
exports.AuthenticationError = AuthenticationError;
|
|
393
|
+
exports.AuthenticationFlowError = AuthenticationFlowError;
|
|
394
|
+
exports.BrowserApprover = BrowserApprover;
|
|
395
|
+
exports.CaidoAuth = CaidoAuth;
|
|
396
|
+
exports.DeviceApprovalError = DeviceApprovalError;
|
|
397
|
+
exports.DeviceInformationError = DeviceInformationError;
|
|
398
|
+
exports.PATApprover = PATApprover;
|
|
399
|
+
exports.TokenRefreshError = TokenRefreshError;
|
|
400
|
+
//# sourceMappingURL=index.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.cjs","names":["Client","fetchExchange"],"sources":["../src/errors.ts","../src/queries.ts","../src/client.ts","../src/approvers/browser.ts","../src/approvers/pat.ts"],"sourcesContent":["/**\n * Base error class for authentication-related errors.\n */\nexport class AuthenticationError extends Error {\n constructor(message: string) {\n super(message);\n this.name = \"AuthenticationError\";\n }\n}\n\n/**\n * Error thrown when the authentication flow fails to start.\n */\nexport class AuthenticationFlowError extends AuthenticationError {\n /** Error code from the API */\n readonly code: string;\n\n constructor(code: string, message: string) {\n super(`${code}: ${message}`);\n this.name = \"AuthenticationFlowError\";\n this.code = code;\n }\n}\n\n/**\n * Error thrown when token refresh fails.\n */\nexport class TokenRefreshError extends AuthenticationError {\n /** Error code from the API */\n readonly code: string;\n\n constructor(code: string, message: string) {\n super(`${code}: ${message}`);\n this.name = \"TokenRefreshError\";\n this.code = code;\n }\n}\n\n/**\n * Error thrown when device approval fails.\n */\nexport class DeviceApprovalError extends AuthenticationError {\n /** HTTP status code if available */\n readonly statusCode: number | undefined;\n\n constructor(message: string, statusCode?: number) {\n super(message);\n this.name = \"DeviceApprovalError\";\n this.statusCode = statusCode;\n }\n}\n\n/**\n * Error thrown when fetching device information fails.\n */\nexport class DeviceInformationError extends AuthenticationError {\n /** HTTP status code if available */\n readonly statusCode: number | undefined;\n\n constructor(message: string, statusCode?: number) {\n super(message);\n this.name = \"DeviceInformationError\";\n this.statusCode = statusCode;\n }\n}\n","import type { DocumentNode } from \"graphql\";\nimport { gql } from \"graphql-tag\";\n\nexport const START_AUTHENTICATION_FLOW: DocumentNode = gql`\n mutation StartAuthenticationFlow {\n startAuthenticationFlow {\n request {\n id\n userCode\n verificationUrl\n expiresAt\n }\n error {\n code\n message\n }\n }\n }\n`;\n\nexport const CREATED_AUTHENTICATION_TOKEN: DocumentNode = gql`\n subscription CreatedAuthenticationToken($requestId: ID!) {\n createdAuthenticationToken(requestId: $requestId) {\n token {\n accessToken\n refreshToken\n expiresAt\n }\n error {\n code\n message\n }\n }\n }\n`;\n\nexport const REFRESH_AUTHENTICATION_TOKEN: DocumentNode = gql`\n mutation RefreshAuthenticationToken($refreshToken: String!) {\n refreshAuthenticationToken(refreshToken: $refreshToken) {\n token {\n accessToken\n refreshToken\n expiresAt\n }\n error {\n code\n message\n }\n }\n }\n`;\n","import { Client, fetchExchange } from \"@urql/core\";\nimport { createClient as createWSClient } from \"graphql-ws\";\n\nimport type { AuthApprover } from \"./approvers/types.js\";\nimport {\n AuthenticationError,\n AuthenticationFlowError,\n TokenRefreshError,\n} from \"./errors.js\";\nimport {\n CREATED_AUTHENTICATION_TOKEN,\n REFRESH_AUTHENTICATION_TOKEN,\n START_AUTHENTICATION_FLOW,\n} from \"./queries.js\";\nimport type {\n AuthenticationRequest,\n AuthenticationToken,\n CreatedAuthenticationTokenResponse,\n RefreshAuthenticationTokenResponse,\n StartAuthenticationFlowResponse,\n} from \"./types.js\";\n\n/**\n * Client for authenticating with a Caido instance.\n *\n * @example\n * ```typescript\n * import { CaidoAuth, BrowserApprover } from \"@caido/auth\";\n *\n * const auth = new CaidoAuth(\n * \"http://localhost:8080\",\n * new BrowserApprover((request) => {\n * console.log(`Visit ${request.verificationUrl}`);\n * })\n * );\n *\n * const token = await auth.startAuthenticationFlow();\n * console.log(\"Access token:\", token.accessToken);\n * ```\n */\nexport class CaidoAuth {\n private readonly instanceUrl: string;\n private readonly graphqlUrl: string;\n private readonly websocketUrl: string;\n private readonly approver: AuthApprover;\n private readonly client: Client;\n\n /**\n * Create a new CaidoAuth client.\n *\n * @param instanceUrl - Base URL of the Caido instance (e.g., \"http://localhost:8080\")\n * @param approver - The approver to use for the authentication flow\n */\n constructor(instanceUrl: string, approver: AuthApprover) {\n this.instanceUrl = instanceUrl.replace(/\\/$/, \"\");\n this.graphqlUrl = `${this.instanceUrl}/graphql`;\n this.websocketUrl = this.getWebsocketUrl();\n this.approver = approver;\n\n this.client = new Client({\n url: this.graphqlUrl,\n exchanges: [fetchExchange],\n });\n }\n\n /**\n * Convert HTTP(S) URL to WS(S) URL for subscriptions.\n */\n private getWebsocketUrl(): string {\n const url = new URL(this.graphqlUrl);\n const scheme = url.protocol === \"https:\" ? \"wss:\" : \"ws:\";\n return `${scheme}//${url.host}/ws/graphql`;\n }\n\n /**\n * Start the device code authentication flow.\n *\n * This method:\n * 1. Initiates the authentication flow via GraphQL mutation\n * 2. Calls the approver with the authentication request\n * 3. Waits for the user to authorize via WebSocket subscription\n * 4. Returns the authentication token once approved\n *\n * @returns The authentication token\n * @throws {AuthenticationFlowError} If the flow fails to start\n * @throws {AuthenticationError} If token retrieval fails\n */\n async startAuthenticationFlow(): Promise<AuthenticationToken> {\n // Step 1: Start the authentication flow\n const result = await this.client\n .mutation<StartAuthenticationFlowResponse>(START_AUTHENTICATION_FLOW, {})\n .toPromise();\n\n if (result.error) {\n throw new AuthenticationFlowError(\"GRAPHQL_ERROR\", result.error.message);\n }\n\n const payload = result.data?.startAuthenticationFlow;\n if (!payload) {\n throw new AuthenticationFlowError(\n \"NO_RESPONSE\",\n \"No response from startAuthenticationFlow\",\n );\n }\n\n if (payload.error) {\n throw new AuthenticationFlowError(\n payload.error.code,\n payload.error.message,\n );\n }\n\n if (!payload.request) {\n throw new AuthenticationFlowError(\n \"NO_REQUEST\",\n \"No authentication request returned\",\n );\n }\n\n const authRequest: AuthenticationRequest = {\n id: payload.request.id,\n userCode: payload.request.userCode,\n verificationUrl: payload.request.verificationUrl,\n expiresAt: new Date(payload.request.expiresAt),\n };\n\n // Step 2: Call the approver\n await this.approver.approve(authRequest);\n\n // Step 3: Wait for the token via subscription\n const token = await this.waitForToken(authRequest.id);\n return token;\n }\n\n /**\n * Subscribe and wait for the authentication token.\n *\n * @param requestId - The authentication request ID\n * @returns The authentication token once the user authorizes\n * @throws {AuthenticationError} If subscription fails or returns an error\n */\n private async waitForToken(requestId: string): Promise<AuthenticationToken> {\n return new Promise<AuthenticationToken>((resolve, reject) => {\n const wsClient = createWSClient({\n url: this.websocketUrl,\n });\n\n const unsubscribe =\n wsClient.subscribe<CreatedAuthenticationTokenResponse>(\n {\n query:\n CREATED_AUTHENTICATION_TOKEN.loc?.source.body ??\n `subscription CreatedAuthenticationToken($requestId: ID!) {\n createdAuthenticationToken(requestId: $requestId) {\n token { accessToken refreshToken expiresAt }\n error { code message }\n }\n }`,\n variables: { requestId },\n },\n {\n next: (result) => {\n const payload = result.data?.createdAuthenticationToken;\n\n if (payload?.error) {\n unsubscribe();\n wsClient.dispose();\n reject(\n new AuthenticationError(\n `${payload.error.code}: ${payload.error.message}`,\n ),\n );\n return;\n }\n\n if (payload?.token) {\n unsubscribe();\n wsClient.dispose();\n resolve({\n accessToken: payload.token.accessToken,\n refreshToken: payload.token.refreshToken,\n expiresAt: new Date(payload.token.expiresAt),\n });\n }\n },\n error: (error) => {\n wsClient.dispose();\n reject(\n new AuthenticationError(\n error instanceof Error ? error.message : String(error),\n ),\n );\n },\n complete: () => {\n wsClient.dispose();\n reject(\n new AuthenticationError(\n \"Subscription ended without receiving token\",\n ),\n );\n },\n },\n );\n });\n }\n\n /**\n * Refresh an access token using a refresh token.\n *\n * @param refreshToken - The refresh token from a previous authentication\n * @returns New authentication token with updated access and refresh tokens\n * @throws {TokenRefreshError} If the refresh fails\n */\n async refreshToken(refreshToken: string): Promise<AuthenticationToken> {\n const result = await this.client\n .mutation<RefreshAuthenticationTokenResponse>(\n REFRESH_AUTHENTICATION_TOKEN,\n { refreshToken },\n )\n .toPromise();\n\n if (result.error) {\n throw new TokenRefreshError(\"GRAPHQL_ERROR\", result.error.message);\n }\n\n const payload = result.data?.refreshAuthenticationToken;\n if (!payload) {\n throw new TokenRefreshError(\n \"NO_RESPONSE\",\n \"No response from refreshAuthenticationToken\",\n );\n }\n\n if (payload.error) {\n throw new TokenRefreshError(payload.error.code, payload.error.message);\n }\n\n if (!payload.token) {\n throw new TokenRefreshError(\"NO_TOKEN\", \"No token returned from refresh\");\n }\n\n return {\n accessToken: payload.token.accessToken,\n refreshToken: payload.token.refreshToken,\n expiresAt: new Date(payload.token.expiresAt),\n };\n }\n}\n","import type { AuthenticationRequest } from \"../types.js\";\n\nimport type { AuthApprover } from \"./types.js\";\n\n/**\n * Callback function that receives the authentication request details.\n * Used to display the verification URL and user code to the user.\n */\nexport type OnRequestCallback = (\n request: AuthenticationRequest,\n) => Promise<void> | void;\n\n/**\n * Browser-based approver that delegates to a callback function.\n * The callback should display the verification URL and user code to the user,\n * who then manually approves the request in their browser.\n *\n * @example\n * ```typescript\n * const approver = new BrowserApprover((request) => {\n * console.log(`Visit ${request.verificationUrl}`);\n * });\n * ```\n */\nexport class BrowserApprover implements AuthApprover {\n private readonly onRequest: OnRequestCallback;\n\n /**\n * Create a new BrowserApprover.\n *\n * @param onRequest - Callback function that will be called with the authentication request\n */\n constructor(onRequest: OnRequestCallback) {\n this.onRequest = onRequest;\n }\n\n /**\n * Approve the authentication request by calling the callback.\n * The actual approval happens when the user visits the URL and enters the code.\n *\n * @param request - The authentication request\n */\n async approve(request: AuthenticationRequest): Promise<void> {\n await this.onRequest(request);\n }\n}\n","import { DeviceApprovalError, DeviceInformationError } from \"../errors.js\";\nimport type { AuthenticationRequest, DeviceInformation } from \"../types.js\";\n\nimport type { AuthApprover } from \"./types.js\";\n\nconst DEFAULT_API_URL = \"https://api.caido.io\";\n\n/**\n * Options for the PATApprover.\n */\nexport interface PATApproverOptions {\n /** The Personal Access Token to use for approval */\n pat: string;\n /** If provided, only approve these scopes. Others will be filtered out. */\n allowedScopes?: string[];\n /** The API URL to use. Defaults to \"https://api.caido.io\" */\n apiUrl?: string;\n}\n\n/**\n * PAT-based approver that automatically approves device code requests.\n * Uses a Personal Access Token to call the Caido API directly.\n *\n * @example\n * ```typescript\n * // Approve all scopes\n * const approver = new PATApprover({ pat: \"caido_xxxxx\" });\n *\n * // Approve only specific scopes\n * const limitedApprover = new PATApprover({\n * pat: \"caido_xxxxx\",\n * allowedScopes: [\"read:projects\", \"write:requests\"],\n * });\n * ```\n */\nexport class PATApprover implements AuthApprover {\n private readonly pat: string;\n private readonly allowedScopes: string[] | undefined;\n private readonly apiUrl: string;\n\n /**\n * Create a new PATApprover.\n *\n * @param options - Configuration options for the approver\n */\n constructor(options: PATApproverOptions) {\n this.pat = options.pat;\n this.allowedScopes = options.allowedScopes;\n this.apiUrl = (options.apiUrl ?? DEFAULT_API_URL).replace(/\\/$/, \"\");\n }\n\n /**\n * Approve the authentication request using the PAT.\n * First fetches device information to get available scopes,\n * then filters scopes if allowedScopes is set,\n * and finally approves the device.\n *\n * @param request - The authentication request\n * @throws {DeviceInformationError} If fetching device information fails\n * @throws {DeviceApprovalError} If approving the device fails\n */\n async approve(request: AuthenticationRequest): Promise<void> {\n // Step 1: Get device information to retrieve available scopes\n const deviceInfo = await this.getDeviceInformation(request.userCode);\n\n // Step 2: Filter scopes if allowedScopes is provided\n let scopesToApprove = deviceInfo.scopes.map((s) => s.name);\n if (this.allowedScopes) {\n scopesToApprove = scopesToApprove.filter((scope) =>\n this.allowedScopes!.includes(scope),\n );\n }\n\n // Step 3: Approve the device with the filtered scopes\n await this.approveDevice(request.userCode, scopesToApprove);\n }\n\n /**\n * Fetch device information from the API.\n *\n * @param userCode - The user code from the authentication request\n * @returns The device information including available scopes\n * @throws {DeviceInformationError} If the request fails\n */\n private async getDeviceInformation(\n userCode: string,\n ): Promise<DeviceInformation> {\n const params = new URLSearchParams();\n params.append(\"user_code\", userCode);\n const url = new URL(`${this.apiUrl}/oauth2/device/information`);\n url.search = params.toString();\n\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n Authorization: `Bearer ${this.pat}`,\n Accept: \"application/json\",\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"Unknown error\");\n throw new DeviceInformationError(\n `Failed to get device information: ${errorText}`,\n response.status,\n );\n }\n\n const data = (await response.json()) as DeviceInformation;\n return data;\n }\n\n /**\n * Approve the device with the specified scopes.\n *\n * @param userCode - The user code from the authentication request\n * @param scopes - The scopes to approve\n * @throws {DeviceApprovalError} If the request fails\n */\n private async approveDevice(\n userCode: string,\n scopes: string[],\n ): Promise<void> {\n const params = new URLSearchParams();\n params.append(\"user_code\", userCode);\n params.append(\"scope\", scopes.join(\",\"));\n const url = new URL(`${this.apiUrl}/oauth2/device/approve`);\n url.search = params.toString();\n\n const response = await fetch(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${this.pat}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n });\n\n if (!response.ok) {\n const errorText = await response.text().catch(() => \"Unknown error\");\n throw new DeviceApprovalError(\n `Failed to approve device: ${errorText}`,\n response.status,\n );\n }\n }\n}\n"],"mappings":"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"}
|