@ttoss/oauth-client 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024, Terezinha Tech Operations (ttoss)
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,131 @@
1
+ # @ttoss/oauth-client
2
+
3
+ Reusable client for connecting a ttoss app to third-party OAuth 2.0 providers
4
+ (TikTok, Instagram, YouTube, Google, …). It covers building the authorization
5
+ URL, exchanging the callback code, and keeping access tokens fresh through lazy
6
+ and scheduled refresh. The client is **ORM- and storage-agnostic**: it operates
7
+ on plain token objects and delegates persistence to you, so it works with
8
+ Sequelize, Prisma, or anything else.
9
+
10
+ See the [OAuth Client guideline](https://ttoss.dev/docs/engineering/guidelines/oauth-third-party-client)
11
+ for the full end-to-end pattern (endpoints, scheduled jobs, internal token access).
12
+
13
+ ## Install
14
+
15
+ ```bash
16
+ pnpm add @ttoss/oauth-client
17
+ ```
18
+
19
+ ## Generic core
20
+
21
+ `createOAuthClient` builds a client for any provider. Parameter-name overrides
22
+ absorb providers that deviate from RFC 6749.
23
+
24
+ ```typescript
25
+ import { createOAuthClient } from '@ttoss/oauth-client';
26
+
27
+ const client = createOAuthClient({
28
+ authorizationUrl: 'https://provider.example/authorize',
29
+ tokenUrl: 'https://provider.example/token',
30
+ clientId: process.env.CLIENT_ID!,
31
+ clientSecret: process.env.CLIENT_SECRET!,
32
+ });
33
+
34
+ // 1. Redirect the user to the provider
35
+ const url = client.buildAuthUrl({
36
+ redirectUri: 'https://app.example/callback',
37
+ scope: ['read', 'write'],
38
+ state: crypto.randomBytes(16).toString('hex'), // persist & verify on callback
39
+ });
40
+
41
+ // 2. Exchange the callback code for tokens
42
+ const tokens = await client.exchangeCode({
43
+ code,
44
+ redirectUri: 'https://app.example/callback',
45
+ });
46
+ ```
47
+
48
+ ## Provider presets
49
+
50
+ Presets pre-configure a provider's quirks. TikTok, for example, names the client
51
+ id `client_key` and uses comma-separated scopes — the preset encodes both.
52
+
53
+ ```typescript
54
+ import { createTikTokClient } from '@ttoss/oauth-client';
55
+
56
+ const tiktok = createTikTokClient({
57
+ clientKey: process.env.TIKTOK_CLIENT_KEY!,
58
+ clientSecret: process.env.TIKTOK_CLIENT_SECRET!,
59
+ });
60
+ ```
61
+
62
+ Adding a provider is one new preset over the same core; nothing else changes.
63
+
64
+ ## Keeping tokens fresh
65
+
66
+ `getValidToken` returns a usable access token, refreshing first if it expires
67
+ within `refreshWindowMs` (default 2h). `onRefresh` receives the new tokens so you
68
+ can persist them.
69
+
70
+ ```typescript
71
+ const accessToken = await client.getValidToken(record, {
72
+ onRefresh: async (updated) => {
73
+ await saveToken(record.id, updated);
74
+ },
75
+ });
76
+ ```
77
+
78
+ `findExpiringTokens` powers a scheduled job that proactively refreshes tokens
79
+ expiring within a wider window (default 6h), keeping connections alive for
80
+ inactive users.
81
+
82
+ ```typescript
83
+ import { findExpiringTokens } from '@ttoss/oauth-client';
84
+
85
+ const expiring = findExpiringTokens(await loadAllTokens());
86
+ await Promise.all(expiring.map(refreshOne));
87
+ ```
88
+
89
+ ## Encryption at rest
90
+
91
+ Access and refresh tokens are credentials — **encrypt them at rest**. The package
92
+ stays encryption-agnostic and works with plaintext records; encrypt at the
93
+ storage boundary with [`@ttoss/auth-core`](https://ttoss.dev/docs/modules/packages/auth-core):
94
+
95
+ ```typescript
96
+ import { decryptValue, encryptValue } from '@ttoss/auth-core';
97
+
98
+ const KEY = process.env.TOKEN_ENCRYPTION_KEY!; // from generateEncryptionKey()
99
+
100
+ const accessToken = await client.getValidToken(
101
+ {
102
+ accessToken: decryptValue({ ciphertext: row.accessToken, key: KEY }),
103
+ refreshToken: decryptValue({ ciphertext: row.refreshToken, key: KEY }),
104
+ accessTokenExpiresAt: row.accessTokenExpiresAt,
105
+ },
106
+ {
107
+ onRefresh: async (updated) => {
108
+ await row.update({
109
+ accessToken: encryptValue({ plaintext: updated.accessToken, key: KEY }),
110
+ refreshToken: encryptValue({
111
+ plaintext: updated.refreshToken,
112
+ key: KEY,
113
+ }),
114
+ accessTokenExpiresAt: new Date(Date.now() + updated.expiresIn * 1000),
115
+ });
116
+ },
117
+ }
118
+ );
119
+ ```
120
+
121
+ ## API
122
+
123
+ | Export | Purpose |
124
+ | --------------------------- | ---------------------------------------------------------------- |
125
+ | `createOAuthClient` | Build a client for a generic `OAuthProvider`. |
126
+ | `createTikTokClient` | Preset for TikTok Login Kit. |
127
+ | `client.buildAuthUrl` | Build the provider's authorization URL. |
128
+ | `client.exchangeCode` | Exchange an authorization code for tokens. |
129
+ | `client.refreshAccessToken` | Refresh an access token from a stored refresh token. |
130
+ | `client.getValidToken` | Return a valid token, refreshing lazily within a safety window. |
131
+ | `findExpiringTokens` | Filter records expiring within a window (for scheduled refresh). |
@@ -0,0 +1,141 @@
1
+
2
+ //#region src/types.d.ts
3
+ /**
4
+ * Overrides for providers that deviate from the RFC 6749 parameter names and
5
+ * conventions. Every field is optional; unset fields fall back to the standard
6
+ * OAuth 2.0 defaults.
7
+ */
8
+ interface ProviderParamOverrides {
9
+ /** Parameter name carrying the client identifier. Default: `client_id`. */
10
+ clientId?: string;
11
+ /** Parameter name carrying the client secret. Default: `client_secret`. */
12
+ clientSecret?: string;
13
+ /** Separator used to join multiple scopes into one string. Default: `' '`. */
14
+ scopeSeparator?: string;
15
+ /** `grant_type` value sent when refreshing a token. Default: `refresh_token`. */
16
+ refreshGrantType?: string;
17
+ }
18
+ /**
19
+ * Normalized token response returned by every client method that talks to the
20
+ * provider's token endpoint. `raw` preserves provider-specific fields (e.g.
21
+ * TikTok's `open_id`) that the generic shape does not model.
22
+ */
23
+ interface TokenResponse {
24
+ /** Short-lived access token used to call the provider's API. */
25
+ accessToken: string;
26
+ /** Long-lived token used to obtain new access tokens. */
27
+ refreshToken: string;
28
+ /** Lifetime of the access token, in seconds. */
29
+ expiresIn: number;
30
+ /** Untouched JSON body from the provider, for provider-specific fields. */
31
+ raw: Record<string, unknown>;
32
+ }
33
+ /**
34
+ * Maps a raw token endpoint JSON body to a {@link TokenResponse}. Supply this on
35
+ * {@link OAuthProvider} when a provider names its token fields differently from
36
+ * the OAuth 2.0 defaults (`access_token`, `refresh_token`, `expires_in`).
37
+ */
38
+ type TokenResponseParser = (raw: Record<string, unknown>) => TokenResponse;
39
+ /**
40
+ * Configuration for a third-party OAuth 2.0 provider. This is the generic,
41
+ * provider-agnostic shape consumed by {@link createOAuthClient}. Pre-built
42
+ * presets (e.g. `createTikTokClient`) assemble this for you.
43
+ */
44
+ interface OAuthProvider {
45
+ /** Authorization endpoint the user is redirected to in order to log in. */
46
+ authorizationUrl: string;
47
+ /** Token endpoint used for both code exchange and refresh. */
48
+ tokenUrl: string;
49
+ /** OAuth client identifier issued by the provider. */
50
+ clientId: string;
51
+ /** OAuth client secret issued by the provider. */
52
+ clientSecret: string;
53
+ /** Overrides for providers that deviate from RFC 6749 parameter names. */
54
+ params?: ProviderParamOverrides;
55
+ /** Custom parser for non-standard token endpoint responses. */
56
+ parseTokenResponse?: TokenResponseParser;
57
+ }
58
+ /**
59
+ * Plain, ORM-agnostic snapshot of a stored token. The package never persists
60
+ * these records itself — the caller loads them (decrypting if stored encrypted)
61
+ * and persists updates via the `onRefresh` callback.
62
+ */
63
+ interface TokenRecord {
64
+ /** Current access token (plaintext). */
65
+ accessToken: string;
66
+ /** Current refresh token (plaintext). */
67
+ refreshToken: string;
68
+ /** Absolute time at which the access token expires. */
69
+ accessTokenExpiresAt: Date;
70
+ }
71
+ //#endregion
72
+ //#region src/client.d.ts
73
+ /** Default safety window for lazy refresh: 2 hours. */
74
+ declare const DEFAULT_REFRESH_WINDOW_MS: number;
75
+ /** Options for {@link OAuthClient.buildAuthUrl}. */
76
+ interface BuildAuthUrlOptions {
77
+ /** Where the provider redirects the user back to after consent. */
78
+ redirectUri: string;
79
+ /** Requested scope(s). An array is joined with the provider's separator. */
80
+ scope: string | string[];
81
+ /** Opaque CSRF value; persist it and verify it on callback. */
82
+ state: string;
83
+ /** Extra provider-specific query parameters to append. */
84
+ extraParams?: Record<string, string>;
85
+ }
86
+ /** Options for {@link OAuthClient.exchangeCode}. */
87
+ interface ExchangeCodeOptions {
88
+ /** Authorization code received on the callback. */
89
+ code: string;
90
+ /** Must match the `redirectUri` used to build the authorization URL. */
91
+ redirectUri: string;
92
+ }
93
+ /** Options for {@link OAuthClient.getValidToken}. */
94
+ interface GetValidTokenOptions {
95
+ /** Refresh if the access token expires within this window. Default: 2h. */
96
+ refreshWindowMs?: number;
97
+ /** Called with the refreshed tokens so the caller can persist them. */
98
+ onRefresh: (updated: TokenResponse) => Promise<void> | void;
99
+ }
100
+ /**
101
+ * A configured OAuth 2.0 client bound to a single provider. Obtain one via
102
+ * {@link createOAuthClient} or a provider preset (e.g. `createTikTokClient`).
103
+ */
104
+ interface OAuthClient {
105
+ /** Build the provider's authorization URL to redirect the user to. */
106
+ buildAuthUrl: (options: BuildAuthUrlOptions) => string;
107
+ /** Exchange an authorization code for tokens at the token endpoint. */
108
+ exchangeCode: (options: ExchangeCodeOptions) => Promise<TokenResponse>;
109
+ /** Refresh an access token using a stored refresh token. */
110
+ refreshAccessToken: (refreshToken: string) => Promise<TokenResponse>;
111
+ /**
112
+ * Return a valid access token, refreshing first if it expires within
113
+ * `refreshWindowMs`. On refresh, `onRefresh` is awaited before returning so
114
+ * the caller can persist the new tokens.
115
+ */
116
+ getValidToken: (record: TokenRecord, options: GetValidTokenOptions) => Promise<string>;
117
+ }
118
+ /**
119
+ * Create an OAuth client for a generic {@link OAuthProvider}. The returned
120
+ * client is stateless and delegates all persistence to its caller, making it
121
+ * ORM- and storage-agnostic.
122
+ */
123
+ declare const createOAuthClient: (provider: OAuthProvider) => OAuthClient;
124
+ //#endregion
125
+ //#region src/providers/tiktok.d.ts
126
+ /** Options for {@link createTikTokClient}. */
127
+ interface TikTokClientOptions {
128
+ /** TikTok app `client_key` (TikTok's name for the client id). */
129
+ clientKey: string;
130
+ /** TikTok app `client_secret`. */
131
+ clientSecret: string;
132
+ }
133
+ /**
134
+ * Build an {@link OAuthClient} preconfigured for TikTok's Login Kit. Encodes
135
+ * TikTok's deviations from RFC 6749: the client id is sent as `client_key` and
136
+ * scopes are comma-separated. The token endpoint returns `open_id` alongside
137
+ * the tokens, available on `TokenResponse.raw`.
138
+ */
139
+ declare const createTikTokClient: (options: TikTokClientOptions) => OAuthClient;
140
+ //#endregion
141
+ export { ExchangeCodeOptions as a, createOAuthClient as c, TokenRecord as d, TokenResponse as f, DEFAULT_REFRESH_WINDOW_MS as i, OAuthProvider as l, createTikTokClient as n, GetValidTokenOptions as o, TokenResponseParser as p, BuildAuthUrlOptions as r, OAuthClient as s, TikTokClientOptions as t, ProviderParamOverrides as u };
@@ -0,0 +1,141 @@
1
+
2
+ //#region src/types.d.ts
3
+ /**
4
+ * Overrides for providers that deviate from the RFC 6749 parameter names and
5
+ * conventions. Every field is optional; unset fields fall back to the standard
6
+ * OAuth 2.0 defaults.
7
+ */
8
+ interface ProviderParamOverrides {
9
+ /** Parameter name carrying the client identifier. Default: `client_id`. */
10
+ clientId?: string;
11
+ /** Parameter name carrying the client secret. Default: `client_secret`. */
12
+ clientSecret?: string;
13
+ /** Separator used to join multiple scopes into one string. Default: `' '`. */
14
+ scopeSeparator?: string;
15
+ /** `grant_type` value sent when refreshing a token. Default: `refresh_token`. */
16
+ refreshGrantType?: string;
17
+ }
18
+ /**
19
+ * Normalized token response returned by every client method that talks to the
20
+ * provider's token endpoint. `raw` preserves provider-specific fields (e.g.
21
+ * TikTok's `open_id`) that the generic shape does not model.
22
+ */
23
+ interface TokenResponse {
24
+ /** Short-lived access token used to call the provider's API. */
25
+ accessToken: string;
26
+ /** Long-lived token used to obtain new access tokens. */
27
+ refreshToken: string;
28
+ /** Lifetime of the access token, in seconds. */
29
+ expiresIn: number;
30
+ /** Untouched JSON body from the provider, for provider-specific fields. */
31
+ raw: Record<string, unknown>;
32
+ }
33
+ /**
34
+ * Maps a raw token endpoint JSON body to a {@link TokenResponse}. Supply this on
35
+ * {@link OAuthProvider} when a provider names its token fields differently from
36
+ * the OAuth 2.0 defaults (`access_token`, `refresh_token`, `expires_in`).
37
+ */
38
+ type TokenResponseParser = (raw: Record<string, unknown>) => TokenResponse;
39
+ /**
40
+ * Configuration for a third-party OAuth 2.0 provider. This is the generic,
41
+ * provider-agnostic shape consumed by {@link createOAuthClient}. Pre-built
42
+ * presets (e.g. `createTikTokClient`) assemble this for you.
43
+ */
44
+ interface OAuthProvider {
45
+ /** Authorization endpoint the user is redirected to in order to log in. */
46
+ authorizationUrl: string;
47
+ /** Token endpoint used for both code exchange and refresh. */
48
+ tokenUrl: string;
49
+ /** OAuth client identifier issued by the provider. */
50
+ clientId: string;
51
+ /** OAuth client secret issued by the provider. */
52
+ clientSecret: string;
53
+ /** Overrides for providers that deviate from RFC 6749 parameter names. */
54
+ params?: ProviderParamOverrides;
55
+ /** Custom parser for non-standard token endpoint responses. */
56
+ parseTokenResponse?: TokenResponseParser;
57
+ }
58
+ /**
59
+ * Plain, ORM-agnostic snapshot of a stored token. The package never persists
60
+ * these records itself — the caller loads them (decrypting if stored encrypted)
61
+ * and persists updates via the `onRefresh` callback.
62
+ */
63
+ interface TokenRecord {
64
+ /** Current access token (plaintext). */
65
+ accessToken: string;
66
+ /** Current refresh token (plaintext). */
67
+ refreshToken: string;
68
+ /** Absolute time at which the access token expires. */
69
+ accessTokenExpiresAt: Date;
70
+ }
71
+ //#endregion
72
+ //#region src/client.d.ts
73
+ /** Default safety window for lazy refresh: 2 hours. */
74
+ declare const DEFAULT_REFRESH_WINDOW_MS: number;
75
+ /** Options for {@link OAuthClient.buildAuthUrl}. */
76
+ interface BuildAuthUrlOptions {
77
+ /** Where the provider redirects the user back to after consent. */
78
+ redirectUri: string;
79
+ /** Requested scope(s). An array is joined with the provider's separator. */
80
+ scope: string | string[];
81
+ /** Opaque CSRF value; persist it and verify it on callback. */
82
+ state: string;
83
+ /** Extra provider-specific query parameters to append. */
84
+ extraParams?: Record<string, string>;
85
+ }
86
+ /** Options for {@link OAuthClient.exchangeCode}. */
87
+ interface ExchangeCodeOptions {
88
+ /** Authorization code received on the callback. */
89
+ code: string;
90
+ /** Must match the `redirectUri` used to build the authorization URL. */
91
+ redirectUri: string;
92
+ }
93
+ /** Options for {@link OAuthClient.getValidToken}. */
94
+ interface GetValidTokenOptions {
95
+ /** Refresh if the access token expires within this window. Default: 2h. */
96
+ refreshWindowMs?: number;
97
+ /** Called with the refreshed tokens so the caller can persist them. */
98
+ onRefresh: (updated: TokenResponse) => Promise<void> | void;
99
+ }
100
+ /**
101
+ * A configured OAuth 2.0 client bound to a single provider. Obtain one via
102
+ * {@link createOAuthClient} or a provider preset (e.g. `createTikTokClient`).
103
+ */
104
+ interface OAuthClient {
105
+ /** Build the provider's authorization URL to redirect the user to. */
106
+ buildAuthUrl: (options: BuildAuthUrlOptions) => string;
107
+ /** Exchange an authorization code for tokens at the token endpoint. */
108
+ exchangeCode: (options: ExchangeCodeOptions) => Promise<TokenResponse>;
109
+ /** Refresh an access token using a stored refresh token. */
110
+ refreshAccessToken: (refreshToken: string) => Promise<TokenResponse>;
111
+ /**
112
+ * Return a valid access token, refreshing first if it expires within
113
+ * `refreshWindowMs`. On refresh, `onRefresh` is awaited before returning so
114
+ * the caller can persist the new tokens.
115
+ */
116
+ getValidToken: (record: TokenRecord, options: GetValidTokenOptions) => Promise<string>;
117
+ }
118
+ /**
119
+ * Create an OAuth client for a generic {@link OAuthProvider}. The returned
120
+ * client is stateless and delegates all persistence to its caller, making it
121
+ * ORM- and storage-agnostic.
122
+ */
123
+ declare const createOAuthClient: (provider: OAuthProvider) => OAuthClient;
124
+ //#endregion
125
+ //#region src/providers/tiktok.d.ts
126
+ /** Options for {@link createTikTokClient}. */
127
+ interface TikTokClientOptions {
128
+ /** TikTok app `client_key` (TikTok's name for the client id). */
129
+ clientKey: string;
130
+ /** TikTok app `client_secret`. */
131
+ clientSecret: string;
132
+ }
133
+ /**
134
+ * Build an {@link OAuthClient} preconfigured for TikTok's Login Kit. Encodes
135
+ * TikTok's deviations from RFC 6749: the client id is sent as `client_key` and
136
+ * scopes are comma-separated. The token endpoint returns `open_id` alongside
137
+ * the tokens, available on `TokenResponse.raw`.
138
+ */
139
+ declare const createTikTokClient: (options: TikTokClientOptions) => OAuthClient;
140
+ //#endregion
141
+ export { ExchangeCodeOptions as a, createOAuthClient as c, TokenRecord as d, TokenResponse as f, DEFAULT_REFRESH_WINDOW_MS as i, OAuthProvider as l, createTikTokClient as n, GetValidTokenOptions as o, TokenResponseParser as p, BuildAuthUrlOptions as r, OAuthClient as s, TikTokClientOptions as t, ProviderParamOverrides as u };
package/dist/index.cjs ADDED
@@ -0,0 +1,29 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ Object.defineProperty(exports, Symbol.toStringTag, {
3
+ value: 'Module'
4
+ });
5
+ const require_providers = require('./providers-FHRUbQH_.cjs');
6
+
7
+ //#region src/tokens.ts
8
+ /** Default scheduled-refresh window: 6 hours. */
9
+ var DEFAULT_EXPIRING_WINDOW_MS = 360 * 60 * 1e3;
10
+ /**
11
+ * Filter a list of token records down to those that expire within `windowMs`
12
+ * from now (default 6 hours). Intended for a scheduled job that proactively
13
+ * refreshes tokens before they lapse, keeping connections alive for inactive
14
+ * users. Pure and provider-agnostic — it inspects only `accessTokenExpiresAt`.
15
+ */
16
+ var findExpiringTokens = (records, options) => {
17
+ const windowMs = options?.windowMs ?? 216e5;
18
+ const threshold = Date.now() + windowMs;
19
+ return records.filter(record => {
20
+ return record.accessTokenExpiresAt.getTime() <= threshold;
21
+ });
22
+ };
23
+
24
+ //#endregion
25
+ exports.DEFAULT_EXPIRING_WINDOW_MS = DEFAULT_EXPIRING_WINDOW_MS;
26
+ exports.DEFAULT_REFRESH_WINDOW_MS = require_providers.DEFAULT_REFRESH_WINDOW_MS;
27
+ exports.createOAuthClient = require_providers.createOAuthClient;
28
+ exports.createTikTokClient = require_providers.createTikTokClient;
29
+ exports.findExpiringTokens = findExpiringTokens;
@@ -0,0 +1,20 @@
1
+
2
+ import { a as ExchangeCodeOptions, c as createOAuthClient, d as TokenRecord, f as TokenResponse, i as DEFAULT_REFRESH_WINDOW_MS, l as OAuthProvider, n as createTikTokClient, o as GetValidTokenOptions, p as TokenResponseParser, r as BuildAuthUrlOptions, s as OAuthClient, t as TikTokClientOptions, u as ProviderParamOverrides } from "./index-BgPLLjdP.cjs";
3
+
4
+ //#region src/tokens.d.ts
5
+ /** Default scheduled-refresh window: 6 hours. */
6
+ declare const DEFAULT_EXPIRING_WINDOW_MS: number;
7
+ /** Options for {@link findExpiringTokens}. */
8
+ interface FindExpiringTokensOptions {
9
+ /** Include records expiring within this window from now. Default: 6h. */
10
+ windowMs?: number;
11
+ }
12
+ /**
13
+ * Filter a list of token records down to those that expire within `windowMs`
14
+ * from now (default 6 hours). Intended for a scheduled job that proactively
15
+ * refreshes tokens before they lapse, keeping connections alive for inactive
16
+ * users. Pure and provider-agnostic — it inspects only `accessTokenExpiresAt`.
17
+ */
18
+ declare const findExpiringTokens: <T extends TokenRecord>(records: T[], options?: FindExpiringTokensOptions) => T[];
19
+ //#endregion
20
+ export { type BuildAuthUrlOptions, DEFAULT_EXPIRING_WINDOW_MS, DEFAULT_REFRESH_WINDOW_MS, type ExchangeCodeOptions, type FindExpiringTokensOptions, type GetValidTokenOptions, type OAuthClient, type OAuthProvider, type ProviderParamOverrides, type TikTokClientOptions, type TokenRecord, type TokenResponse, type TokenResponseParser, createOAuthClient, createTikTokClient, findExpiringTokens };
@@ -0,0 +1,20 @@
1
+
2
+ import { a as ExchangeCodeOptions, c as createOAuthClient, d as TokenRecord, f as TokenResponse, i as DEFAULT_REFRESH_WINDOW_MS, l as OAuthProvider, n as createTikTokClient, o as GetValidTokenOptions, p as TokenResponseParser, r as BuildAuthUrlOptions, s as OAuthClient, t as TikTokClientOptions, u as ProviderParamOverrides } from "./index-BgPLLjdP.mjs";
3
+
4
+ //#region src/tokens.d.ts
5
+ /** Default scheduled-refresh window: 6 hours. */
6
+ declare const DEFAULT_EXPIRING_WINDOW_MS: number;
7
+ /** Options for {@link findExpiringTokens}. */
8
+ interface FindExpiringTokensOptions {
9
+ /** Include records expiring within this window from now. Default: 6h. */
10
+ windowMs?: number;
11
+ }
12
+ /**
13
+ * Filter a list of token records down to those that expire within `windowMs`
14
+ * from now (default 6 hours). Intended for a scheduled job that proactively
15
+ * refreshes tokens before they lapse, keeping connections alive for inactive
16
+ * users. Pure and provider-agnostic — it inspects only `accessTokenExpiresAt`.
17
+ */
18
+ declare const findExpiringTokens: <T extends TokenRecord>(records: T[], options?: FindExpiringTokensOptions) => T[];
19
+ //#endregion
20
+ export { type BuildAuthUrlOptions, DEFAULT_EXPIRING_WINDOW_MS, DEFAULT_REFRESH_WINDOW_MS, type ExchangeCodeOptions, type FindExpiringTokensOptions, type GetValidTokenOptions, type OAuthClient, type OAuthProvider, type ProviderParamOverrides, type TikTokClientOptions, type TokenRecord, type TokenResponse, type TokenResponseParser, createOAuthClient, createTikTokClient, findExpiringTokens };
package/dist/index.mjs ADDED
@@ -0,0 +1,22 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ import { n as DEFAULT_REFRESH_WINDOW_MS, r as createOAuthClient, t as createTikTokClient } from "./providers-Cxs2-TSE.mjs";
3
+
4
+ //#region src/tokens.ts
5
+ /** Default scheduled-refresh window: 6 hours. */
6
+ var DEFAULT_EXPIRING_WINDOW_MS = 360 * 60 * 1e3;
7
+ /**
8
+ * Filter a list of token records down to those that expire within `windowMs`
9
+ * from now (default 6 hours). Intended for a scheduled job that proactively
10
+ * refreshes tokens before they lapse, keeping connections alive for inactive
11
+ * users. Pure and provider-agnostic — it inspects only `accessTokenExpiresAt`.
12
+ */
13
+ var findExpiringTokens = (records, options) => {
14
+ const windowMs = options?.windowMs ?? 216e5;
15
+ const threshold = Date.now() + windowMs;
16
+ return records.filter(record => {
17
+ return record.accessTokenExpiresAt.getTime() <= threshold;
18
+ });
19
+ };
20
+
21
+ //#endregion
22
+ export { DEFAULT_EXPIRING_WINDOW_MS, DEFAULT_REFRESH_WINDOW_MS, createOAuthClient, createTikTokClient, findExpiringTokens };
@@ -0,0 +1,6 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ Object.defineProperty(exports, Symbol.toStringTag, {
3
+ value: 'Module'
4
+ });
5
+ const require_providers = require('../providers-FHRUbQH_.cjs');
6
+ exports.createTikTokClient = require_providers.createTikTokClient;
@@ -0,0 +1,3 @@
1
+
2
+ import { n as createTikTokClient, t as TikTokClientOptions } from "../index-BgPLLjdP.cjs";
3
+ export { type TikTokClientOptions, createTikTokClient };
@@ -0,0 +1,3 @@
1
+
2
+ import { n as createTikTokClient, t as TikTokClientOptions } from "../index-BgPLLjdP.mjs";
3
+ export { type TikTokClientOptions, createTikTokClient };
@@ -0,0 +1,3 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ import { t as createTikTokClient } from "../providers-Cxs2-TSE.mjs";
3
+ export { createTikTokClient };
@@ -0,0 +1,107 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ //#region src/client.ts
3
+ /** Default safety window for lazy refresh: 2 hours. */
4
+ var DEFAULT_REFRESH_WINDOW_MS = 7200 * 1e3;
5
+ var defaultParseTokenResponse = raw => {
6
+ return {
7
+ accessToken: String(raw.access_token),
8
+ refreshToken: String(raw.refresh_token),
9
+ expiresIn: Number(raw.expires_in),
10
+ raw
11
+ };
12
+ };
13
+ /**
14
+ * Create an OAuth client for a generic {@link OAuthProvider}. The returned
15
+ * client is stateless and delegates all persistence to its caller, making it
16
+ * ORM- and storage-agnostic.
17
+ */
18
+ var createOAuthClient = provider => {
19
+ const clientIdParam = provider.params?.clientId ?? "client_id";
20
+ const clientSecretParam = provider.params?.clientSecret ?? "client_secret";
21
+ const scopeSeparator = provider.params?.scopeSeparator ?? " ";
22
+ const refreshGrantType = provider.params?.refreshGrantType ?? "refresh_token";
23
+ const parse = provider.parseTokenResponse ?? defaultParseTokenResponse;
24
+ const requestToken = async body => {
25
+ const response = await fetch(provider.tokenUrl, {
26
+ method: "POST",
27
+ headers: {
28
+ "Content-Type": "application/x-www-form-urlencoded"
29
+ },
30
+ body
31
+ });
32
+ if (!response.ok) {
33
+ const text = await response.text();
34
+ throw new Error(`OAuth token request failed: ${response.status} ${response.statusText} ${text}`);
35
+ }
36
+ return parse(await response.json());
37
+ };
38
+ const refreshAccessToken = refreshToken => {
39
+ return requestToken(new URLSearchParams({
40
+ [clientIdParam]: provider.clientId,
41
+ [clientSecretParam]: provider.clientSecret,
42
+ grant_type: refreshGrantType,
43
+ refresh_token: refreshToken
44
+ }));
45
+ };
46
+ return {
47
+ buildAuthUrl: ({
48
+ redirectUri,
49
+ scope,
50
+ state,
51
+ extraParams
52
+ }) => {
53
+ const url = new URL(provider.authorizationUrl);
54
+ url.searchParams.set(clientIdParam, provider.clientId);
55
+ url.searchParams.set("response_type", "code");
56
+ url.searchParams.set("redirect_uri", redirectUri);
57
+ url.searchParams.set("scope", Array.isArray(scope) ? scope.join(scopeSeparator) : scope);
58
+ url.searchParams.set("state", state);
59
+ for (const [key, value] of Object.entries(extraParams ?? {})) url.searchParams.set(key, value);
60
+ return url.toString();
61
+ },
62
+ exchangeCode: ({
63
+ code,
64
+ redirectUri
65
+ }) => {
66
+ return requestToken(new URLSearchParams({
67
+ [clientIdParam]: provider.clientId,
68
+ [clientSecretParam]: provider.clientSecret,
69
+ grant_type: "authorization_code",
70
+ code,
71
+ redirect_uri: redirectUri
72
+ }));
73
+ },
74
+ refreshAccessToken,
75
+ getValidToken: async (record, options) => {
76
+ const refreshWindowMs = options.refreshWindowMs ?? 72e5;
77
+ if (record.accessTokenExpiresAt.getTime() - Date.now() > refreshWindowMs) return record.accessToken;
78
+ const updated = await refreshAccessToken(record.refreshToken);
79
+ await options.onRefresh(updated);
80
+ return updated.accessToken;
81
+ }
82
+ };
83
+ };
84
+
85
+ //#endregion
86
+ //#region src/providers/tiktok.ts
87
+ /**
88
+ * Build an {@link OAuthClient} preconfigured for TikTok's Login Kit. Encodes
89
+ * TikTok's deviations from RFC 6749: the client id is sent as `client_key` and
90
+ * scopes are comma-separated. The token endpoint returns `open_id` alongside
91
+ * the tokens, available on `TokenResponse.raw`.
92
+ */
93
+ var createTikTokClient = options => {
94
+ return createOAuthClient({
95
+ authorizationUrl: "https://www.tiktok.com/v2/auth/authorize/",
96
+ tokenUrl: "https://open.tiktokapis.com/v2/oauth/token/",
97
+ clientId: options.clientKey,
98
+ clientSecret: options.clientSecret,
99
+ params: {
100
+ clientId: "client_key",
101
+ scopeSeparator: ","
102
+ }
103
+ });
104
+ };
105
+
106
+ //#endregion
107
+ export { DEFAULT_REFRESH_WINDOW_MS as n, createOAuthClient as r, createTikTokClient as t };
@@ -0,0 +1,124 @@
1
+ /** Powered by @ttoss/config. https://ttoss.dev/docs/modules/packages/config/ */
2
+ //#region src/client.ts
3
+ /** Default safety window for lazy refresh: 2 hours. */
4
+ var DEFAULT_REFRESH_WINDOW_MS = 7200 * 1e3;
5
+ var defaultParseTokenResponse = raw => {
6
+ return {
7
+ accessToken: String(raw.access_token),
8
+ refreshToken: String(raw.refresh_token),
9
+ expiresIn: Number(raw.expires_in),
10
+ raw
11
+ };
12
+ };
13
+ /**
14
+ * Create an OAuth client for a generic {@link OAuthProvider}. The returned
15
+ * client is stateless and delegates all persistence to its caller, making it
16
+ * ORM- and storage-agnostic.
17
+ */
18
+ var createOAuthClient = provider => {
19
+ const clientIdParam = provider.params?.clientId ?? "client_id";
20
+ const clientSecretParam = provider.params?.clientSecret ?? "client_secret";
21
+ const scopeSeparator = provider.params?.scopeSeparator ?? " ";
22
+ const refreshGrantType = provider.params?.refreshGrantType ?? "refresh_token";
23
+ const parse = provider.parseTokenResponse ?? defaultParseTokenResponse;
24
+ const requestToken = async body => {
25
+ const response = await fetch(provider.tokenUrl, {
26
+ method: "POST",
27
+ headers: {
28
+ "Content-Type": "application/x-www-form-urlencoded"
29
+ },
30
+ body
31
+ });
32
+ if (!response.ok) {
33
+ const text = await response.text();
34
+ throw new Error(`OAuth token request failed: ${response.status} ${response.statusText} ${text}`);
35
+ }
36
+ return parse(await response.json());
37
+ };
38
+ const refreshAccessToken = refreshToken => {
39
+ return requestToken(new URLSearchParams({
40
+ [clientIdParam]: provider.clientId,
41
+ [clientSecretParam]: provider.clientSecret,
42
+ grant_type: refreshGrantType,
43
+ refresh_token: refreshToken
44
+ }));
45
+ };
46
+ return {
47
+ buildAuthUrl: ({
48
+ redirectUri,
49
+ scope,
50
+ state,
51
+ extraParams
52
+ }) => {
53
+ const url = new URL(provider.authorizationUrl);
54
+ url.searchParams.set(clientIdParam, provider.clientId);
55
+ url.searchParams.set("response_type", "code");
56
+ url.searchParams.set("redirect_uri", redirectUri);
57
+ url.searchParams.set("scope", Array.isArray(scope) ? scope.join(scopeSeparator) : scope);
58
+ url.searchParams.set("state", state);
59
+ for (const [key, value] of Object.entries(extraParams ?? {})) url.searchParams.set(key, value);
60
+ return url.toString();
61
+ },
62
+ exchangeCode: ({
63
+ code,
64
+ redirectUri
65
+ }) => {
66
+ return requestToken(new URLSearchParams({
67
+ [clientIdParam]: provider.clientId,
68
+ [clientSecretParam]: provider.clientSecret,
69
+ grant_type: "authorization_code",
70
+ code,
71
+ redirect_uri: redirectUri
72
+ }));
73
+ },
74
+ refreshAccessToken,
75
+ getValidToken: async (record, options) => {
76
+ const refreshWindowMs = options.refreshWindowMs ?? 72e5;
77
+ if (record.accessTokenExpiresAt.getTime() - Date.now() > refreshWindowMs) return record.accessToken;
78
+ const updated = await refreshAccessToken(record.refreshToken);
79
+ await options.onRefresh(updated);
80
+ return updated.accessToken;
81
+ }
82
+ };
83
+ };
84
+
85
+ //#endregion
86
+ //#region src/providers/tiktok.ts
87
+ /**
88
+ * Build an {@link OAuthClient} preconfigured for TikTok's Login Kit. Encodes
89
+ * TikTok's deviations from RFC 6749: the client id is sent as `client_key` and
90
+ * scopes are comma-separated. The token endpoint returns `open_id` alongside
91
+ * the tokens, available on `TokenResponse.raw`.
92
+ */
93
+ var createTikTokClient = options => {
94
+ return createOAuthClient({
95
+ authorizationUrl: "https://www.tiktok.com/v2/auth/authorize/",
96
+ tokenUrl: "https://open.tiktokapis.com/v2/oauth/token/",
97
+ clientId: options.clientKey,
98
+ clientSecret: options.clientSecret,
99
+ params: {
100
+ clientId: "client_key",
101
+ scopeSeparator: ","
102
+ }
103
+ });
104
+ };
105
+
106
+ //#endregion
107
+ Object.defineProperty(exports, 'DEFAULT_REFRESH_WINDOW_MS', {
108
+ enumerable: true,
109
+ get: function () {
110
+ return DEFAULT_REFRESH_WINDOW_MS;
111
+ }
112
+ });
113
+ Object.defineProperty(exports, 'createOAuthClient', {
114
+ enumerable: true,
115
+ get: function () {
116
+ return createOAuthClient;
117
+ }
118
+ });
119
+ Object.defineProperty(exports, 'createTikTokClient', {
120
+ enumerable: true,
121
+ get: function () {
122
+ return createTikTokClient;
123
+ }
124
+ });
package/package.json ADDED
@@ -0,0 +1,53 @@
1
+ {
2
+ "name": "@ttoss/oauth-client",
3
+ "version": "0.1.0",
4
+ "description": "Reusable third-party OAuth 2.0 client for ttoss — authorization, code exchange, lazy and scheduled token refresh",
5
+ "keywords": [
6
+ "auth",
7
+ "oauth",
8
+ "oauth2",
9
+ "tiktok",
10
+ "ttoss"
11
+ ],
12
+ "repository": {
13
+ "type": "git",
14
+ "url": "https://github.com/ttoss/ttoss.git",
15
+ "directory": "packages/oauth-client"
16
+ },
17
+ "license": "MIT",
18
+ "author": "ttoss",
19
+ "contributors": [
20
+ "Pedro Arantes <pedro@arantespp.com> (https://arantespp.com)"
21
+ ],
22
+ "sideEffects": false,
23
+ "type": "module",
24
+ "exports": {
25
+ ".": {
26
+ "import": "./dist/index.mjs",
27
+ "require": "./dist/index.cjs",
28
+ "types": "./dist/index.d.mts"
29
+ },
30
+ "./providers": {
31
+ "import": "./dist/providers/index.mjs",
32
+ "require": "./dist/providers/index.cjs",
33
+ "types": "./dist/providers/index.d.mts"
34
+ }
35
+ },
36
+ "files": [
37
+ "dist"
38
+ ],
39
+ "devDependencies": {
40
+ "jest": "^30.4.2",
41
+ "tsdown": "^0.22.2",
42
+ "@ttoss/auth-core": "^0.7.0",
43
+ "@ttoss/config": "^1.37.17"
44
+ },
45
+ "publishConfig": {
46
+ "access": "public",
47
+ "provenance": true
48
+ },
49
+ "scripts": {
50
+ "build": "tsdown",
51
+ "test": "jest --projects tests/unit"
52
+ }
53
+ }