@rakomi/node 0.0.0 → 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 +21 -0
- package/README.md +57 -1
- package/SECURITY.md +206 -0
- package/dist/agents.d.ts +90 -0
- package/dist/agents.js +203 -0
- package/dist/anonymous.d.ts +50 -0
- package/dist/anonymous.js +105 -0
- package/dist/ciba.d.ts +97 -0
- package/dist/ciba.js +282 -0
- package/dist/client.d.ts +93 -0
- package/dist/client.js +202 -0
- package/dist/credentials.d.ts +87 -0
- package/dist/credentials.js +104 -0
- package/dist/device.d.ts +76 -0
- package/dist/device.js +244 -0
- package/dist/doctor.d.ts +11 -0
- package/dist/doctor.js +135 -0
- package/dist/dpop-session.d.ts +90 -0
- package/dist/dpop-session.js +127 -0
- package/dist/dpop.d.ts +24 -0
- package/dist/dpop.js +51 -0
- package/dist/env-detect.d.ts +11 -0
- package/dist/env-detect.js +26 -0
- package/dist/errors.d.ts +307 -0
- package/dist/errors.js +385 -0
- package/dist/eudi.d.ts +23 -0
- package/dist/eudi.js +27 -0
- package/dist/flags.d.ts +50 -0
- package/dist/flags.js +173 -0
- package/dist/guards.d.ts +16 -0
- package/dist/guards.js +104 -0
- package/dist/index.d.ts +30 -0
- package/dist/index.js +18 -0
- package/dist/internal/canonical-url.d.ts +13 -0
- package/dist/internal/canonical-url.js +52 -0
- package/dist/internal/shared-constants.d.ts +3 -0
- package/dist/internal/shared-constants.js +3 -0
- package/dist/jwks-cache.d.ts +31 -0
- package/dist/jwks-cache.js +135 -0
- package/dist/link.d.ts +73 -0
- package/dist/link.js +262 -0
- package/dist/middleware.d.ts +21 -0
- package/dist/middleware.js +84 -0
- package/dist/oauth.d.ts +46 -0
- package/dist/oauth.js +457 -0
- package/dist/rbac.d.ts +12 -0
- package/dist/rbac.js +20 -0
- package/dist/token-exchange.d.ts +65 -0
- package/dist/token-exchange.js +163 -0
- package/dist/types.d.ts +436 -0
- package/dist/types.js +1 -0
- package/dist/verify-publisher-webhook.d.ts +25 -0
- package/dist/verify-publisher-webhook.js +47 -0
- package/dist/verify-token.d.ts +3 -0
- package/dist/verify-token.js +148 -0
- package/dist/verify-webhook.d.ts +7 -0
- package/dist/verify-webhook.js +101 -0
- package/package.json +61 -5
- package/sbom.cdx.json +52 -0
package/dist/link.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Node SDK surface for user-scoped account linking.
|
|
3
|
+
*
|
|
4
|
+
* Wraps the three `/v1/users/me/link*` endpoints.
|
|
5
|
+
* These endpoints require an end-user JWT (NOT an API key), so each helper
|
|
6
|
+
* takes a `{ userToken }` option. The SDK client (constructed with an API key)
|
|
7
|
+
* only carries the `baseUrl`; the user token flows per-call.
|
|
8
|
+
*
|
|
9
|
+
* Returns the shared `VerifyResult` shape — NEVER throws on expected 4xx.
|
|
10
|
+
* Typed error classes (AccountLinkingDisabledError, IdentityOwnedByOtherUserError,
|
|
11
|
+
* CannotUnlinkLastMethodError) are also exported for callers that prefer
|
|
12
|
+
* pattern-matching via `instanceof` over inspecting `error.code`.
|
|
13
|
+
*/
|
|
14
|
+
import { ACCOUNT_LINKING_IDENTITY_NOT_FOUND, ACCOUNT_LINKING_NETWORK_ERROR, ACCOUNT_LINKING_RATE_LIMITED, AccountLinkingDisabledError, CannotUnlinkLastMethodError, CooldownActiveError, IdentityOwnedByOtherUserError, LinkStateExpiredError, MfaStepUpRequiredError, MfaStepUpUnavailableError, } from './errors.js';
|
|
15
|
+
function parseRetryAfter(res) {
|
|
16
|
+
const retryAfter = res.headers.get('retry-after');
|
|
17
|
+
if (!retryAfter)
|
|
18
|
+
return undefined;
|
|
19
|
+
const n = Number(retryAfter);
|
|
20
|
+
if (Number.isFinite(n) && n > 0)
|
|
21
|
+
return Math.round(n);
|
|
22
|
+
const ts = Date.parse(retryAfter);
|
|
23
|
+
if (!Number.isFinite(ts))
|
|
24
|
+
return undefined;
|
|
25
|
+
const delta = Math.round((ts - Date.now()) / 1000);
|
|
26
|
+
return delta > 0 ? delta : undefined;
|
|
27
|
+
}
|
|
28
|
+
async function safeJson(res) {
|
|
29
|
+
try {
|
|
30
|
+
return (await res.json());
|
|
31
|
+
}
|
|
32
|
+
catch {
|
|
33
|
+
return null;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
async function parseErrorBody(res) {
|
|
37
|
+
try {
|
|
38
|
+
return (await res.json());
|
|
39
|
+
}
|
|
40
|
+
catch {
|
|
41
|
+
return {};
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* User-scoped account-linking resource. Attached to `RakomiClient#link`.
|
|
46
|
+
*
|
|
47
|
+
* All methods require an end-user JWT passed via `{ userToken }`. The underlying
|
|
48
|
+
* `RakomiClient` API key is NOT sent on these calls — the API rejects API-key
|
|
49
|
+
* auth on user-scoped routes.
|
|
50
|
+
*/
|
|
51
|
+
export class LinkClient {
|
|
52
|
+
baseUrl;
|
|
53
|
+
fetchImpl;
|
|
54
|
+
constructor(ctx) {
|
|
55
|
+
this.baseUrl = ctx.baseUrl;
|
|
56
|
+
this.fetchImpl = ctx.fetchImpl ?? fetch;
|
|
57
|
+
}
|
|
58
|
+
/** GET /v1/users/me/link — list linked methods for the authenticated user. */
|
|
59
|
+
async list(options) {
|
|
60
|
+
let res;
|
|
61
|
+
try {
|
|
62
|
+
res = await this.fetchImpl(`${this.baseUrl}/v1/users/me/link`, {
|
|
63
|
+
method: 'GET',
|
|
64
|
+
headers: {
|
|
65
|
+
Authorization: `Bearer ${options.userToken}`,
|
|
66
|
+
Accept: 'application/json',
|
|
67
|
+
},
|
|
68
|
+
redirect: 'error',
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
catch (err) {
|
|
72
|
+
return {
|
|
73
|
+
ok: false,
|
|
74
|
+
error: ACCOUNT_LINKING_NETWORK_ERROR(err instanceof Error ? err.message : undefined),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (res.status === 200) {
|
|
78
|
+
const body = await safeJson(res);
|
|
79
|
+
if (!body || !Array.isArray(body.methods)) {
|
|
80
|
+
return { ok: false, error: ACCOUNT_LINKING_NETWORK_ERROR('malformed response body') };
|
|
81
|
+
}
|
|
82
|
+
return { ok: true, data: body };
|
|
83
|
+
}
|
|
84
|
+
return { ok: false, error: await this.mapError(res, 'list') };
|
|
85
|
+
}
|
|
86
|
+
/** POST /v1/users/me/link/{provider} — initiate OAuth link flow. */
|
|
87
|
+
async initiate(provider, options) {
|
|
88
|
+
const body = { redirect_uri: options.redirectUri };
|
|
89
|
+
if (options.mfaVerificationToken) {
|
|
90
|
+
body.mfa_verification_token = options.mfaVerificationToken;
|
|
91
|
+
}
|
|
92
|
+
let res;
|
|
93
|
+
try {
|
|
94
|
+
res = await this.fetchImpl(`${this.baseUrl}/v1/users/me/link/${encodeURIComponent(provider)}`, {
|
|
95
|
+
method: 'POST',
|
|
96
|
+
headers: {
|
|
97
|
+
Authorization: `Bearer ${options.userToken}`,
|
|
98
|
+
'Content-Type': 'application/json',
|
|
99
|
+
Accept: 'application/json',
|
|
100
|
+
},
|
|
101
|
+
body: JSON.stringify(body),
|
|
102
|
+
redirect: 'error',
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
return {
|
|
107
|
+
ok: false,
|
|
108
|
+
error: ACCOUNT_LINKING_NETWORK_ERROR(err instanceof Error ? err.message : undefined),
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (res.status === 200) {
|
|
112
|
+
const body = await safeJson(res);
|
|
113
|
+
if (!body || typeof body.authorization_url !== 'string') {
|
|
114
|
+
return { ok: false, error: ACCOUNT_LINKING_NETWORK_ERROR('malformed response body') };
|
|
115
|
+
}
|
|
116
|
+
if (!/^https?:\/\//i.test(body.authorization_url)) {
|
|
117
|
+
return { ok: false, error: ACCOUNT_LINKING_NETWORK_ERROR('invalid authorization_url scheme') };
|
|
118
|
+
}
|
|
119
|
+
return { ok: true, data: body };
|
|
120
|
+
}
|
|
121
|
+
return { ok: false, error: await this.mapError(res, 'initiate') };
|
|
122
|
+
}
|
|
123
|
+
/** DELETE /v1/users/me/link/{provider} — unlink a social identity. */
|
|
124
|
+
async remove(provider, options) {
|
|
125
|
+
let res;
|
|
126
|
+
try {
|
|
127
|
+
res = await this.fetchImpl(`${this.baseUrl}/v1/users/me/link/${encodeURIComponent(provider)}`, {
|
|
128
|
+
method: 'DELETE',
|
|
129
|
+
headers: {
|
|
130
|
+
Authorization: `Bearer ${options.userToken}`,
|
|
131
|
+
Accept: 'application/json',
|
|
132
|
+
},
|
|
133
|
+
redirect: 'error',
|
|
134
|
+
});
|
|
135
|
+
}
|
|
136
|
+
catch (err) {
|
|
137
|
+
return {
|
|
138
|
+
ok: false,
|
|
139
|
+
error: ACCOUNT_LINKING_NETWORK_ERROR(err instanceof Error ? err.message : undefined),
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (res.status === 200) {
|
|
143
|
+
const body = await safeJson(res);
|
|
144
|
+
if (!body || typeof body.unlinked !== 'boolean') {
|
|
145
|
+
return { ok: false, error: ACCOUNT_LINKING_NETWORK_ERROR('malformed response body') };
|
|
146
|
+
}
|
|
147
|
+
return { ok: true, data: body };
|
|
148
|
+
}
|
|
149
|
+
return { ok: false, error: await this.mapError(res, 'remove') };
|
|
150
|
+
}
|
|
151
|
+
async mapError(res, op) {
|
|
152
|
+
const body = await parseErrorBody(res);
|
|
153
|
+
const code = body.code ?? '';
|
|
154
|
+
switch (res.status) {
|
|
155
|
+
case 400:
|
|
156
|
+
if (code === 'account_linking/link_state_expired_or_missing') {
|
|
157
|
+
const err = new LinkStateExpiredError(body.message);
|
|
158
|
+
return sdkErrorFromClass(err);
|
|
159
|
+
}
|
|
160
|
+
break;
|
|
161
|
+
case 401:
|
|
162
|
+
if (code === 'account_linking/mfa_step_up_unavailable') {
|
|
163
|
+
const err = new MfaStepUpUnavailableError('passwordless_user_no_step_up_route', body.message);
|
|
164
|
+
return sdkErrorFromClass(err, { reason: err.reason });
|
|
165
|
+
}
|
|
166
|
+
if (code === 'account_linking/mfa_required') {
|
|
167
|
+
const challenge = typeof body.mfa_challenge_token === 'string'
|
|
168
|
+
? body.mfa_challenge_token
|
|
169
|
+
: typeof body.details?.mfa_challenge_token === 'string'
|
|
170
|
+
? body.details.mfa_challenge_token
|
|
171
|
+
: 'mfa_step_up_required';
|
|
172
|
+
const availableRaw = Array.isArray(body.available_methods)
|
|
173
|
+
? body.available_methods
|
|
174
|
+
: Array.isArray(body.details?.available_methods)
|
|
175
|
+
? body.details.available_methods
|
|
176
|
+
: undefined;
|
|
177
|
+
const availableMethods = availableRaw && availableRaw.length > 0
|
|
178
|
+
? availableRaw.filter((m) => typeof m === 'string')
|
|
179
|
+
: undefined;
|
|
180
|
+
const err = new MfaStepUpRequiredError(challenge, body.message, availableMethods);
|
|
181
|
+
return sdkErrorFromClass(err, {
|
|
182
|
+
next_action: err.next_action,
|
|
183
|
+
...(availableMethods ? { available_methods: [...availableMethods] } : {}),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return {
|
|
187
|
+
code: 'account_linking/unauthorized',
|
|
188
|
+
message: body.message || 'User token is missing, expired, or invalid.',
|
|
189
|
+
suggestion: 'Re-authenticate the end user and retry with a fresh access token.',
|
|
190
|
+
docs_url: 'https://docs.rakomi.dev/sdk/errors#account_linking-unauthorized',
|
|
191
|
+
};
|
|
192
|
+
case 403:
|
|
193
|
+
if (code === 'account_linking/disabled_for_tenant') {
|
|
194
|
+
const err = new AccountLinkingDisabledError(body.message);
|
|
195
|
+
return sdkErrorFromClass(err);
|
|
196
|
+
}
|
|
197
|
+
return {
|
|
198
|
+
code: code || 'account_linking/forbidden',
|
|
199
|
+
message: body.message || 'Forbidden',
|
|
200
|
+
suggestion: 'The end user is not permitted to perform this account-linking operation.',
|
|
201
|
+
docs_url: 'https://docs.rakomi.dev/sdk/errors#account_linking-forbidden',
|
|
202
|
+
};
|
|
203
|
+
case 404:
|
|
204
|
+
if (code === 'account_linking/identity_not_found' || (op === 'remove' && !code)) {
|
|
205
|
+
return ACCOUNT_LINKING_IDENTITY_NOT_FOUND();
|
|
206
|
+
}
|
|
207
|
+
return {
|
|
208
|
+
code: code || 'account_linking/not_found',
|
|
209
|
+
message: body.message || `HTTP 404`,
|
|
210
|
+
suggestion: 'The requested resource was not found.',
|
|
211
|
+
docs_url: 'https://docs.rakomi.dev/sdk/errors#account_linking-not_found',
|
|
212
|
+
};
|
|
213
|
+
case 409:
|
|
214
|
+
if (code === 'account_linking/cannot_unlink_last_method') {
|
|
215
|
+
const err = new CannotUnlinkLastMethodError(body.details?.remaining_methods ?? [], body.message);
|
|
216
|
+
return sdkErrorFromClass(err, { remaining_methods: err.remaining_methods });
|
|
217
|
+
}
|
|
218
|
+
if (code === 'account_linking/identity_owned_by_other_user') {
|
|
219
|
+
const err = new IdentityOwnedByOtherUserError(body.message);
|
|
220
|
+
return sdkErrorFromClass(err);
|
|
221
|
+
}
|
|
222
|
+
break;
|
|
223
|
+
case 429:
|
|
224
|
+
if (code === 'account_linking/cooldown_active') {
|
|
225
|
+
const unlockAt = typeof body.details?.unlock_at === 'string'
|
|
226
|
+
? body.details.unlock_at
|
|
227
|
+
: new Date(Date.now() + 60 * 60 * 1000).toISOString();
|
|
228
|
+
const reason = body.details?.reason === 'account_recently_linked'
|
|
229
|
+
? 'account_recently_linked'
|
|
230
|
+
: 'unknown';
|
|
231
|
+
const err = new CooldownActiveError(unlockAt, reason, body.message);
|
|
232
|
+
return sdkErrorFromClass(err, {
|
|
233
|
+
unlock_at: err.unlockAtIso,
|
|
234
|
+
reason: err.reason,
|
|
235
|
+
});
|
|
236
|
+
}
|
|
237
|
+
return ACCOUNT_LINKING_RATE_LIMITED(parseRetryAfter(res));
|
|
238
|
+
}
|
|
239
|
+
const truncated = (body.message ?? '').slice(0, 200);
|
|
240
|
+
return ACCOUNT_LINKING_NETWORK_ERROR(truncated || `HTTP ${res.status}`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
const DOCS_BASE = 'https://docs.rakomi.dev/sdk/errors';
|
|
244
|
+
/**
|
|
245
|
+
* Convert a typed Error class instance into the SDK's SdkError object shape.
|
|
246
|
+
* Callers who want the typed class can still `instanceof` the exported classes;
|
|
247
|
+
* this bridge lets the Result-returning APIs emit structured errors without
|
|
248
|
+
* throwing.
|
|
249
|
+
*/
|
|
250
|
+
function sdkErrorFromClass(err, extra) {
|
|
251
|
+
const base = {
|
|
252
|
+
code: err.code,
|
|
253
|
+
message: err.message,
|
|
254
|
+
suggestion: err.suggestion,
|
|
255
|
+
docs_url: err.docs_url ?? `${DOCS_BASE}#${err.code.replace('/', '-')}`,
|
|
256
|
+
};
|
|
257
|
+
if (extra) {
|
|
258
|
+
for (const [k, v] of Object.entries(extra))
|
|
259
|
+
base[k] = v;
|
|
260
|
+
}
|
|
261
|
+
return base;
|
|
262
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { MiddlewareOptions, SdkEnvironment, TokenPayload, VerifyResult } from './types.js';
|
|
2
|
+
export interface MiddlewareRequest {
|
|
3
|
+
headers: Record<string, string | string[] | undefined>;
|
|
4
|
+
}
|
|
5
|
+
export interface MiddlewareResponse {
|
|
6
|
+
status(code: number): MiddlewareResponse;
|
|
7
|
+
json(body: unknown): void;
|
|
8
|
+
setHeader(name: string, value: string): void;
|
|
9
|
+
}
|
|
10
|
+
export type NextFunction = (error?: unknown) => void;
|
|
11
|
+
type VerifyTokenFn = (token: string) => Promise<VerifyResult<TokenPayload>>;
|
|
12
|
+
/**
|
|
13
|
+
* Resolve whether to include verbose error details (suggestions, fix commands).
|
|
14
|
+
* SECURITY: NEVER trust the Host header — a spoofed
|
|
15
|
+
* `Host: localhost` in production would leak permission/role names.
|
|
16
|
+
* Uses ONLY the `environmentOverride` config parameter set at SDK init time.
|
|
17
|
+
* Safe default: production (non-verbose).
|
|
18
|
+
*/
|
|
19
|
+
export declare function resolveVerbose(_req: MiddlewareRequest, environmentOverride?: SdkEnvironment): boolean;
|
|
20
|
+
export declare function createMiddleware(verifyTokenFn: VerifyTokenFn, options?: MiddlewareOptions, environmentOverride?: SdkEnvironment): (req: MiddlewareRequest, res: MiddlewareResponse, next: NextFunction) => void;
|
|
21
|
+
export {};
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
const BEARER_PREFIX = 'bearer ';
|
|
2
|
+
function formatWireError(sdkError, verbose) {
|
|
3
|
+
if (verbose) {
|
|
4
|
+
return {
|
|
5
|
+
error: {
|
|
6
|
+
code: sdkError.code,
|
|
7
|
+
message: sdkError.message,
|
|
8
|
+
docs_url: sdkError.docs_url,
|
|
9
|
+
suggestion: sdkError.suggestion,
|
|
10
|
+
fix_command: sdkError.fix_command,
|
|
11
|
+
},
|
|
12
|
+
};
|
|
13
|
+
}
|
|
14
|
+
return {
|
|
15
|
+
error: {
|
|
16
|
+
code: sdkError.code,
|
|
17
|
+
message: sdkError.message,
|
|
18
|
+
docs_url: sdkError.docs_url,
|
|
19
|
+
},
|
|
20
|
+
};
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Resolve whether to include verbose error details (suggestions, fix commands).
|
|
24
|
+
* SECURITY: NEVER trust the Host header — a spoofed
|
|
25
|
+
* `Host: localhost` in production would leak permission/role names.
|
|
26
|
+
* Uses ONLY the `environmentOverride` config parameter set at SDK init time.
|
|
27
|
+
* Safe default: production (non-verbose).
|
|
28
|
+
*/
|
|
29
|
+
export function resolveVerbose(_req, environmentOverride) {
|
|
30
|
+
if (environmentOverride) {
|
|
31
|
+
return environmentOverride === 'development';
|
|
32
|
+
}
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
export function createMiddleware(verifyTokenFn, options, environmentOverride) {
|
|
36
|
+
return (req, res, next) => {
|
|
37
|
+
const verbose = resolveVerbose(req, environmentOverride);
|
|
38
|
+
void (async () => {
|
|
39
|
+
try {
|
|
40
|
+
const authorization = req.headers['authorization'] ?? req.headers['Authorization'];
|
|
41
|
+
const authHeader = typeof authorization === 'string' ? authorization : undefined;
|
|
42
|
+
if (!authHeader || !authHeader.toLowerCase().startsWith(BEARER_PREFIX)) {
|
|
43
|
+
const error = {
|
|
44
|
+
code: 'token/missing',
|
|
45
|
+
message: 'Authorization header with Bearer token is required',
|
|
46
|
+
suggestion: 'Include an Authorization header: Bearer <token>',
|
|
47
|
+
docs_url: 'https://docs.rakomi.dev/sdk/errors#token-missing',
|
|
48
|
+
};
|
|
49
|
+
if (options?.onError) {
|
|
50
|
+
options.onError(error, req, res);
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
res.status(401).json(formatWireError(error, verbose));
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
const token = authHeader.slice(BEARER_PREFIX.length);
|
|
57
|
+
const result = await verifyTokenFn(token);
|
|
58
|
+
if (!result.ok) {
|
|
59
|
+
if (options?.onError) {
|
|
60
|
+
options.onError(result.error, req, res);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
res.status(401).json(formatWireError(result.error, verbose));
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
req.auth = result.data;
|
|
67
|
+
next();
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
const error = {
|
|
71
|
+
code: 'token/internal_error',
|
|
72
|
+
message: 'An internal error occurred during token verification',
|
|
73
|
+
suggestion: 'This is unexpected. Please retry or contact support',
|
|
74
|
+
docs_url: 'https://docs.rakomi.dev/sdk/errors#token-internal_error',
|
|
75
|
+
};
|
|
76
|
+
if (options?.onError) {
|
|
77
|
+
options.onError(error, req, res);
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
res.status(401).json(formatWireError(error, verbose));
|
|
81
|
+
}
|
|
82
|
+
})();
|
|
83
|
+
};
|
|
84
|
+
}
|
package/dist/oauth.d.ts
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import type { AuthorizeUrlOptions, OAuthExchangeOptions, OAuthRefreshOptions, OAuthRotateOptions, OAuthTokenResponse, PkceChallenge, RotationTokenResponse, VerifyResult } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Generate a PKCE code verifier and challenge pair.
|
|
4
|
+
* Uses node:crypto for secure random generation.
|
|
5
|
+
*/
|
|
6
|
+
export declare function generatePKCE(): PkceChallenge;
|
|
7
|
+
/**
|
|
8
|
+
* Generate a random state parameter for CSRF protection.
|
|
9
|
+
* Returns 32 random bytes, hex-encoded.
|
|
10
|
+
*/
|
|
11
|
+
export declare function generateState(): string;
|
|
12
|
+
/**
|
|
13
|
+
* Build a full /oauth/authorize URL with all required parameters.
|
|
14
|
+
* Pure function — no config dependency, usable without RakomiClient instance.
|
|
15
|
+
*/
|
|
16
|
+
export declare function buildAuthorizeUrl(options: AuthorizeUrlOptions): string;
|
|
17
|
+
/**
|
|
18
|
+
* Exchange an authorization code for tokens via POST /oauth/token.
|
|
19
|
+
* Never throws — returns VerifyResult<OAuthTokenResponse>.
|
|
20
|
+
*/
|
|
21
|
+
export declare function exchangeCode(options: OAuthExchangeOptions): Promise<VerifyResult<OAuthTokenResponse>>;
|
|
22
|
+
/**
|
|
23
|
+
* Refresh an OAuth token via POST /oauth/token.
|
|
24
|
+
* Serializes concurrent calls with the same refresh token to prevent nuclear revocation.
|
|
25
|
+
* Never throws — returns VerifyResult<OAuthTokenResponse>.
|
|
26
|
+
*/
|
|
27
|
+
export declare function refreshToken(options: OAuthRefreshOptions): Promise<VerifyResult<OAuthTokenResponse>>;
|
|
28
|
+
/**
|
|
29
|
+
* Perform an in-band DPoP refresh-key ROTATION via POST /oauth/token
|
|
30
|
+
* Co-presents the OLD-key proof (the
|
|
31
|
+
* session's current key, on the primary `DPoP` header — backward-safe) and a
|
|
32
|
+
* fresh NEW-key proof (on the `DPoP-Rotate` header) on ONE refresh request, then
|
|
33
|
+
* atomically swaps the session's active prover to the new key ONLY after the
|
|
34
|
+
* server confirms a 200 whose access-token `cnf.jkt` EQUALS the new key's `jkt`
|
|
35
|
+
* (the master invariant). Any other outcome keeps the OLD key bound (fail-SAFE,
|
|
36
|
+
* never a half-swap) and surfaces a distinct non-success signal.
|
|
37
|
+
*
|
|
38
|
+
* This is a DEDICATED ceremony, not a flag on {@link refreshToken}: the second
|
|
39
|
+
* key is created only inside this call frame, so a `DPoP-Rotate` header is
|
|
40
|
+
* structurally impossible to attach to an ordinary refresh. Single-flight
|
|
41
|
+
* per session — a concurrent rotation coalesces. Never throws; always resolves to
|
|
42
|
+
* a `VerifyResult`.
|
|
43
|
+
*
|
|
44
|
+
* @public — additive-only after the first public release.
|
|
45
|
+
*/
|
|
46
|
+
export declare function rotateRefreshKey(options: OAuthRotateOptions): Promise<VerifyResult<RotationTokenResponse>>;
|