@42paris/intraoapi42 1.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.
package/README.md ADDED
@@ -0,0 +1,262 @@
1
+ # intraoapi42
2
+
3
+ Typed TypeScript client for the [42 Intra API](https://api.intra.42.fr/), built on [`openapi-fetch`](https://openapi-ts.dev/packages/openapi-fetch/).
4
+
5
+ Features:
6
+
7
+ - Fully typed requests and responses generated from the OpenAPI spec.
8
+ - Automatic OAuth2 client credentials flow with token refresh.
9
+ - Built‑in retry logic for transient failures.
10
+ - Ready‑made configs for production and staging environments.
11
+
12
+ ---
13
+
14
+ ## Installation
15
+
16
+ ```bash
17
+ npm install intraoapi42
18
+ ```
19
+
20
+ or with a scoped name if you publish as such:
21
+
22
+ ```bash
23
+ npm install @your-username/intraoapi42
24
+ ```
25
+
26
+ Requires Node.js with ESM support (your `package.json` should have `"type": "module"` or use `.mjs` extensions).
27
+
28
+ ---
29
+
30
+ ## Quick start
31
+
32
+ ```ts
33
+ import {
34
+ createApiClient,
35
+ ProductionConfig,
36
+ withClientCredentials,
37
+ withScopes,
38
+ } from "intraoapi42";
39
+
40
+ // Configure OAuth2 client credentials
41
+ const config = withClientCredentials(
42
+ ProductionConfig,
43
+ "YOUR_CLIENT_ID",
44
+ "YOUR_CLIENT_SECRET",
45
+ );
46
+
47
+ // Optionally restrict scopes
48
+ const scopedConfig = withScopes(
49
+ config,
50
+ "public",
51
+ "projects",
52
+ );
53
+
54
+ // Create the API client
55
+ const api = createApiClient(scopedConfig);
56
+
57
+ // Example: GET /v2/users/:id
58
+ const { data, error } = await api.GET("/users/{id}", {
59
+ params: {
60
+ path: { id: 12345 },
61
+ },
62
+ });
63
+
64
+ if (error) {
65
+ throw new Error(`API error: ${error.message}`);
66
+ }
67
+
68
+ console.log(data);
69
+ ```
70
+
71
+ Types for paths, parameters, and responses are inferred from the OpenAPI spec, so you get full TypeScript autocomplete and type checking.
72
+
73
+ ---
74
+
75
+ ## Configuration
76
+
77
+ ### Environments
78
+
79
+ Two built‑in configs are provided:
80
+
81
+ ```ts
82
+ import { ProductionConfig, StagingConfig } from "intraoapi42";
83
+
84
+ ProductionConfig;
85
+ // {
86
+ // tokenUrl: "https://api.intra.42.fr/oauth/token",
87
+ // serverUrl: "https://api.intra.42.fr/v2",
88
+ // }
89
+
90
+ StagingConfig;
91
+ // {
92
+ // tokenUrl: "https://api.intra-staging.42.fr/oauth/token",
93
+ // serverUrl: "https://api.intra-staging.42.fr/v2",
94
+ // }
95
+ ```
96
+
97
+ ### Adding credentials
98
+
99
+ Use `withClientCredentials` to attach your OAuth2 client ID and secret:
100
+
101
+ ```ts
102
+ import {
103
+ ProductionConfig,
104
+ withClientCredentials,
105
+ createApiClient,
106
+ } from "intraoapi42";
107
+
108
+ const config = withClientCredentials(
109
+ ProductionConfig,
110
+ process.env.INTRA_CLIENT_ID!,
111
+ process.env.INTRA_CLIENT_SECRET!,
112
+ );
113
+
114
+ const api = createApiClient(config);
115
+ ```
116
+
117
+ ### Adding scopes
118
+
119
+ Optionally restrict the token’s scopes:
120
+
121
+ ```ts
122
+ import { withScopes } from "intraoapi42";
123
+
124
+ const config = withScopes(
125
+ ProductionConfig,
126
+ "public",
127
+ "projects",
128
+ "activities",
129
+ );
130
+ ```
131
+
132
+ Scopes are passed to the token endpoint as a space‑separated string.
133
+
134
+ ---
135
+
136
+ ## How authentication works
137
+
138
+ `createApiClient` sets up:
139
+
140
+ - A **refreshable token source** that:
141
+ - Requests a new access token using client credentials when needed.
142
+ - Caches the token until close to expiry (with a 60s safety margin).
143
+ - Deduplicates concurrent token requests.
144
+ - An **auth middleware** that:
145
+ - Adds `Authorization: Bearer <token>` to every request.
146
+ - On `401 Unauthorized`, invalidates the cached token, fetches a fresh one, and retries the request once.
147
+
148
+ You don’t need to manage tokens manually; just use the client.
149
+
150
+ ---
151
+
152
+ ## Usage patterns
153
+
154
+ ### List users with pagination
155
+
156
+ ```ts
157
+ const { data, error } = await api.GET("/users", {
158
+ params: {
159
+ query: {
160
+ page_size: 50,
161
+ page: 1,
162
+ },
163
+ },
164
+ });
165
+
166
+ if (error) {
167
+ throw new Error(error.message);
168
+ }
169
+
170
+ // data is typed according to the OpenAPI spec
171
+ console.log(data);
172
+ ```
173
+
174
+ ### POST / PATCH / DELETE
175
+
176
+ ```ts
177
+ // Example: update a user
178
+ const { data, error } = await api.PATCH("/users/{id}", {
179
+ params: {
180
+ path: { id: 12345 },
181
+ },
182
+ body: {
183
+ // typed fields here
184
+ displayname: "New Name",
185
+ },
186
+ });
187
+ ```
188
+
189
+ All HTTP methods (`GET`, `POST`, `PUT`, `PATCH`, `DELETE`, etc.) are available with full typing.
190
+
191
+ ---
192
+
193
+ ## Error handling
194
+
195
+ Each call returns `{ data, error }`:
196
+
197
+ - `data` is defined when the request succeeds.
198
+ - `error` is defined when the request fails (network error, non‑2xx response, etc.).
199
+
200
+ ```ts
201
+ const { data, error } = await api.GET("/users/{id}", {
202
+ params: { path: { id: 12345 } },
203
+ });
204
+
205
+ if (error) {
206
+ // error has shape: { message, body?, response, ... }
207
+ console.error("Status:", error.response.status);
208
+ console.error("Body:", await error.response.clone().text());
209
+ throw new Error("Failed to fetch user");
210
+ }
211
+
212
+ // Use data safely
213
+ console.log(data.id, data.login);
214
+ ```
215
+
216
+ Refer to `openapi-fetch` docs for the exact error shape and advanced patterns.
217
+
218
+ ---
219
+
220
+ ## TypeScript usage
221
+
222
+ The package exports types generated from the OpenAPI spec:
223
+
224
+ ```ts
225
+ import type { paths } from "intraoapi42";
226
+
227
+ // paths describes all available endpoints and their shapes
228
+ type UsersEndpoint = paths["/users"];
229
+ ```
230
+
231
+ Your IDE will infer types automatically from `api.GET`, `api.POST`, etc., so you usually don’t need to import these manually.
232
+
233
+ ---
234
+
235
+ ## Development / Contributing
236
+
237
+ If you’re working on the client itself:
238
+
239
+ ```bash
240
+ cd clients/typescript
241
+
242
+ # Install dependencies
243
+ npm install
244
+
245
+ # Generate types from OpenAPI spec
246
+ npm run generate:api
247
+
248
+ # Typecheck
249
+ npm run typecheck
250
+
251
+ # Build
252
+ npm run build
253
+
254
+ # Run tests
255
+ npm test
256
+ ```
257
+
258
+ ---
259
+
260
+ ## License
261
+
262
+ MIT
@@ -0,0 +1,14 @@
1
+ import type { paths } from "./types.js";
2
+ export type Config = {
3
+ tokenUrl: string;
4
+ serverUrl: string;
5
+ clientId?: string;
6
+ clientSecret?: string;
7
+ scopes?: string[];
8
+ };
9
+ export declare const ProductionConfig: Config;
10
+ export declare const StagingConfig: Config;
11
+ export declare function withClientCredentials(config: Config, clientId: string, clientSecret: string): Config;
12
+ export declare function withScopes(config: Config, ...scopes: string[]): Config;
13
+ export declare function createApiClient(config: Config): import("openapi-fetch").Client<paths, `${string}/${string}`>;
14
+ //# sourceMappingURL=client.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,YAAY,CAAC;AAUxC,MAAM,MAAM,MAAM,GAAG;IACnB,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,eAAO,MAAM,gBAAgB,EAAE,MAG9B,CAAC;AAEF,eAAO,MAAM,aAAa,EAAE,MAG3B,CAAC;AAEF,wBAAgB,qBAAqB,CACnC,MAAM,EAAE,MAAM,EACd,QAAQ,EAAE,MAAM,EAChB,YAAY,EAAE,MAAM,GACnB,MAAM,CAMR;AAED,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,EAAE,MAAM,EAAE,GAAG,MAAM,CAKtE;AAwGD,wBAAgB,eAAe,CAAC,MAAM,EAAE,MAAM,gEAsC7C"}
package/dist/client.js ADDED
@@ -0,0 +1,127 @@
1
+ import createClient, {} from "openapi-fetch";
2
+ import { retryFetch } from "./retry-fetch.js";
3
+ export const ProductionConfig = {
4
+ tokenUrl: "https://api.intra.42.fr/oauth/token",
5
+ serverUrl: "https://api.intra.42.fr/v2",
6
+ };
7
+ export const StagingConfig = {
8
+ tokenUrl: "https://api.intra-staging.42.fr/oauth/token",
9
+ serverUrl: "https://api.intra-staging.42.fr/v2",
10
+ };
11
+ export function withClientCredentials(config, clientId, clientSecret) {
12
+ return {
13
+ ...config,
14
+ clientId,
15
+ clientSecret,
16
+ };
17
+ }
18
+ export function withScopes(config, ...scopes) {
19
+ return {
20
+ ...config,
21
+ scopes,
22
+ };
23
+ }
24
+ class RefreshableTokenSource {
25
+ config;
26
+ accessToken;
27
+ expiresAt = 0;
28
+ pendingRequest;
29
+ constructor(config) {
30
+ this.config = config;
31
+ }
32
+ async token() {
33
+ const now = Date.now();
34
+ if (this.accessToken !== undefined &&
35
+ now < this.expiresAt - 60_000) {
36
+ return this.accessToken;
37
+ }
38
+ if (this.pendingRequest !== undefined) {
39
+ return this.pendingRequest;
40
+ }
41
+ const request = this.fetchToken();
42
+ this.pendingRequest = request;
43
+ try {
44
+ return await request;
45
+ }
46
+ finally {
47
+ if (this.pendingRequest === request) {
48
+ this.pendingRequest = undefined;
49
+ }
50
+ }
51
+ }
52
+ invalidate() {
53
+ this.accessToken = undefined;
54
+ this.expiresAt = 0;
55
+ }
56
+ async fetchToken() {
57
+ const { clientId, clientSecret } = this.config;
58
+ if (clientId === undefined || clientSecret === undefined) {
59
+ throw new Error("OAuth client credentials are missing");
60
+ }
61
+ const params = new URLSearchParams({
62
+ client_id: clientId,
63
+ client_secret: clientSecret,
64
+ grant_type: "client_credentials",
65
+ });
66
+ if (this.config.scopes !== undefined && this.config.scopes.length > 0) {
67
+ params.set("scope", this.config.scopes.join(" "));
68
+ }
69
+ const response = await fetch(this.config.tokenUrl, {
70
+ method: "POST",
71
+ headers: {
72
+ Accept: "application/json",
73
+ "Content-Type": "application/x-www-form-urlencoded",
74
+ },
75
+ body: params,
76
+ });
77
+ if (!response.ok) {
78
+ throw new Error(`OAuth token request failed: ${response.status} ${await response.text()}`);
79
+ }
80
+ const payload = await response.json();
81
+ if (!isOAuthTokenResponse(payload)) {
82
+ throw new Error("OAuth token response did not contain a valid access_token");
83
+ }
84
+ this.accessToken = payload.access_token;
85
+ this.expiresAt =
86
+ Date.now() + (payload.expires_in ?? 3_600) * 1_000;
87
+ return payload.access_token;
88
+ }
89
+ }
90
+ function isOAuthTokenResponse(value) {
91
+ if (typeof value !== "object" || value === null) {
92
+ return false;
93
+ }
94
+ return ("access_token" in value &&
95
+ typeof value.access_token === "string" &&
96
+ value.access_token.length > 0 &&
97
+ "token_type" in value &&
98
+ typeof value.token_type === "string");
99
+ }
100
+ export function createApiClient(config) {
101
+ const tokenSource = new RefreshableTokenSource(config);
102
+ const client = createClient({
103
+ baseUrl: config.serverUrl,
104
+ fetch: retryFetch,
105
+ });
106
+ const authMiddleware = {
107
+ async onRequest({ request }) {
108
+ const token = await tokenSource.token();
109
+ request.headers.set("Authorization", `Bearer ${token}`);
110
+ return request;
111
+ },
112
+ async onResponse({ request, response }) {
113
+ if (response.status !== 401) {
114
+ return response;
115
+ }
116
+ await response.body?.cancel();
117
+ tokenSource.invalidate();
118
+ const retryRequest = request.clone();
119
+ const token = await tokenSource.token();
120
+ retryRequest.headers.set("Authorization", `Bearer ${token}`);
121
+ return retryFetch(retryRequest);
122
+ },
123
+ };
124
+ client.use(authMiddleware);
125
+ return client;
126
+ }
127
+ //# sourceMappingURL=client.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.js","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,3 @@
1
+ export { createApiClient } from './client.js';
2
+ export * from './types.js';
3
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,cAAc,YAAY,CAAC"}
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { createApiClient } from './client.js';
2
+ export * from './types.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAE,MAAM,aAAa,CAAC;AAC9C,cAAc,YAAY,CAAC"}
@@ -0,0 +1,7 @@
1
+ export type RetryFetchOptions = {
2
+ maxAttempts?: number;
3
+ baseDelayMs?: number;
4
+ maxDelayMs?: number;
5
+ };
6
+ export declare function retryFetch(input: RequestInfo | URL, init?: RequestInit): Promise<Response>;
7
+ //# sourceMappingURL=retry-fetch.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-fetch.d.ts","sourceRoot":"","sources":["../src/retry-fetch.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,iBAAiB,GAAG;IAC9B,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB,CAAC;AAYF,wBAAsB,UAAU,CAC9B,KAAK,EAAE,WAAW,GAAG,GAAG,EACxB,IAAI,CAAC,EAAE,WAAW,GACjB,OAAO,CAAC,QAAQ,CAAC,CAuCnB"}
@@ -0,0 +1,56 @@
1
+ const RETRYABLE_STATUS_CODES = new Set([
2
+ 408,
3
+ 425,
4
+ 429,
5
+ 500,
6
+ 502,
7
+ 503,
8
+ 504,
9
+ ]);
10
+ export async function retryFetch(input, init) {
11
+ const maxAttempts = 3;
12
+ const baseDelayMs = 250;
13
+ const maxDelayMs = 5_000;
14
+ for (let attempt = 0; attempt < maxAttempts; attempt += 1) {
15
+ try {
16
+ const response = await fetch(input, init);
17
+ const shouldRetry = RETRYABLE_STATUS_CODES.has(response.status) &&
18
+ attempt < maxAttempts - 1;
19
+ if (!shouldRetry) {
20
+ return response;
21
+ }
22
+ const retryAfterMs = getRetryAfterMs(response);
23
+ const exponentialDelay = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
24
+ await delay(retryAfterMs ?? exponentialDelay);
25
+ }
26
+ catch (error) {
27
+ if (attempt >= maxAttempts - 1) {
28
+ throw error;
29
+ }
30
+ const exponentialDelay = Math.min(maxDelayMs, baseDelayMs * 2 ** attempt);
31
+ await delay(exponentialDelay);
32
+ }
33
+ }
34
+ throw new Error("Retry loop terminated unexpectedly");
35
+ }
36
+ function getRetryAfterMs(response) {
37
+ const value = response.headers.get("retry-after");
38
+ if (value === null) {
39
+ return undefined;
40
+ }
41
+ const seconds = Number(value);
42
+ if (Number.isFinite(seconds)) {
43
+ return Math.max(0, seconds * 1_000);
44
+ }
45
+ const date = Date.parse(value);
46
+ if (Number.isNaN(date)) {
47
+ return undefined;
48
+ }
49
+ return Math.max(0, date - Date.now());
50
+ }
51
+ function delay(milliseconds) {
52
+ return new Promise((resolve) => {
53
+ setTimeout(resolve, milliseconds);
54
+ });
55
+ }
56
+ //# sourceMappingURL=retry-fetch.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retry-fetch.js","sourceRoot":"","sources":["../src/retry-fetch.ts"],"names":[],"mappings":"AAMA,MAAM,sBAAsB,GAAG,IAAI,GAAG,CAAC;IACrC,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;IACH,GAAG;CACJ,CAAC,CAAC;AAEH,MAAM,CAAC,KAAK,UAAU,UAAU,CAC9B,KAAwB,EACxB,IAAkB;IAElB,MAAM,WAAW,GAAG,CAAC,CAAC;IACtB,MAAM,WAAW,GAAG,GAAG,CAAC;IACxB,MAAM,UAAU,GAAG,KAAK,CAAC;IAEzB,KAAK,IAAI,OAAO,GAAG,CAAC,EAAE,OAAO,GAAG,WAAW,EAAE,OAAO,IAAI,CAAC,EAAE,CAAC;QAC1D,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,CAAC;YAE1C,MAAM,WAAW,GACf,sBAAsB,CAAC,GAAG,CAAC,QAAQ,CAAC,MAAM,CAAC;gBAC3C,OAAO,GAAG,WAAW,GAAG,CAAC,CAAC;YAE5B,IAAI,CAAC,WAAW,EAAE,CAAC;gBACjB,OAAO,QAAQ,CAAC;YAClB,CAAC;YAED,MAAM,YAAY,GAAG,eAAe,CAAC,QAAQ,CAAC,CAAC;YAC/C,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAC/B,UAAU,EACV,WAAW,GAAG,CAAC,IAAI,OAAO,CAC3B,CAAC;YAEF,MAAM,KAAK,CAAC,YAAY,IAAI,gBAAgB,CAAC,CAAC;QAChD,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,IAAI,OAAO,IAAI,WAAW,GAAG,CAAC,EAAE,CAAC;gBAC/B,MAAM,KAAK,CAAC;YACd,CAAC;YAED,MAAM,gBAAgB,GAAG,IAAI,CAAC,GAAG,CAC/B,UAAU,EACV,WAAW,GAAG,CAAC,IAAI,OAAO,CAC3B,CAAC;YAEF,MAAM,KAAK,CAAC,gBAAgB,CAAC,CAAC;QAChC,CAAC;IACH,CAAC;IAED,MAAM,IAAI,KAAK,CAAC,oCAAoC,CAAC,CAAC;AACxD,CAAC;AAED,SAAS,eAAe,CAAC,QAAkB;IACzC,MAAM,KAAK,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,CAAC;IAElD,IAAI,KAAK,KAAK,IAAI,EAAE,CAAC;QACnB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,MAAM,OAAO,GAAG,MAAM,CAAC,KAAK,CAAC,CAAC;IAE9B,IAAI,MAAM,CAAC,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;QAC7B,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,OAAO,GAAG,KAAK,CAAC,CAAC;IACtC,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;IAE/B,IAAI,MAAM,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,CAAC;QACvB,OAAO,SAAS,CAAC;IACnB,CAAC;IAED,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;AACxC,CAAC;AAED,SAAS,KAAK,CAAC,YAAoB;IACjC,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE;QAC7B,UAAU,CAAC,OAAO,EAAE,YAAY,CAAC,CAAC;IACpC,CAAC,CAAC,CAAC;AACL,CAAC"}