@omelhorsite/sdk 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/README.md +321 -0
- package/dist/index.js +11589 -0
- package/dist/types/auth/device.d.ts +156 -0
- package/dist/types/auth/index.d.ts +127 -0
- package/dist/types/auth/tokens.d.ts +356 -0
- package/dist/types/client.d.ts +133 -0
- package/dist/types/errors.d.ts +202 -0
- package/dist/types/http.d.ts +204 -0
- package/dist/types/index.d.ts +33 -0
- package/dist/types/local/index.d.ts +42 -0
- package/dist/types/local/password.d.ts +169 -0
- package/dist/types/local/qr.d.ts +127 -0
- package/dist/types/local/wordlist.d.ts +26 -0
- package/dist/types/resources/account.d.ts +296 -0
- package/dist/types/resources/chests.d.ts +194 -0
- package/dist/types/resources/dynamicQrs.d.ts +172 -0
- package/dist/types/resources/forms.d.ts +331 -0
- package/dist/types/resources/index.d.ts +30 -0
- package/dist/types/resources/ipLookup.d.ts +63 -0
- package/dist/types/resources/jobs.d.ts +233 -0
- package/dist/types/resources/linkTrees.d.ts +249 -0
- package/dist/types/resources/notepads.d.ts +96 -0
- package/dist/types/resources/shortLinks.d.ts +248 -0
- package/dist/types/resources/storage/upload.d.ts +459 -0
- package/dist/types/resources/storage.d.ts +527 -0
- package/dist/types/resources/tickets.d.ts +236 -0
- package/dist/types/resources/tools/backgroundRemoval.d.ts +99 -0
- package/dist/types/resources/tools/captions.d.ts +318 -0
- package/dist/types/resources/tools/downloader.d.ts +397 -0
- package/dist/types/resources/tools/index.d.ts +215 -0
- package/dist/types/resources/tools/jumpstyle.d.ts +194 -0
- package/dist/types/resources/tools/transcription.d.ts +178 -0
- package/dist/types/resources/tools/upscale.d.ts +94 -0
- package/dist/types/resources/tools/vocalSeparation.d.ts +183 -0
- package/dist/types/types.d.ts +245 -0
- package/package.json +37 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* OAuth 2.0 Device Authorization Grant (RFC 8628).
|
|
3
|
+
*
|
|
4
|
+
* This is how a CLI or an MCP server signs a person in without ever handling
|
|
5
|
+
* their password: the client asks for a code, the person opens a URL in a real
|
|
6
|
+
* browser and approves, and the client polls the token endpoint until the
|
|
7
|
+
* approval lands.
|
|
8
|
+
*
|
|
9
|
+
* The approval page is rendered by RAILS at `backend.omelhorsite.pt`, not by
|
|
10
|
+
* the Next.js frontend - the browser already holds the host-only `oms_session`
|
|
11
|
+
* cookie for that origin, so the person is usually already signed in when they
|
|
12
|
+
* arrive.
|
|
13
|
+
*
|
|
14
|
+
* The core never opens a browser and never prints a URL. It returns the
|
|
15
|
+
* verification URI and lets the host decide what to do with it.
|
|
16
|
+
*/
|
|
17
|
+
import { Resource } from "../http";
|
|
18
|
+
import type { RequestOptions } from "../types";
|
|
19
|
+
import { OmsOAuthError, type TokenSet } from "./tokens";
|
|
20
|
+
/** The RFC 8628 `grant_type` URN. Sent literally; `URLSearchParams` escapes it. */
|
|
21
|
+
export declare const DEVICE_CODE_GRANT_TYPE = "urn:ietf:params:oauth:grant-type:device_code";
|
|
22
|
+
/** Poll interval to use when the server names none. Matches the backend default. */
|
|
23
|
+
export declare const DEFAULT_DEVICE_INTERVAL_MS = 5000;
|
|
24
|
+
/** Grant lifetime to assume when the caller passes no `expiresAt`. */
|
|
25
|
+
export declare const DEFAULT_DEVICE_EXPIRY_MS = 600000;
|
|
26
|
+
/** How much `slow_down` adds to the interval, per RFC 8628 §3.5. Permanent. */
|
|
27
|
+
export declare const DEVICE_SLOW_DOWN_STEP_MS = 5000;
|
|
28
|
+
/** What the device authorization endpoint answers. */
|
|
29
|
+
export interface DeviceAuthorization {
|
|
30
|
+
/** Opaque code the client polls with. Never show it to the person. */
|
|
31
|
+
readonly deviceCode: string;
|
|
32
|
+
/** Short code the person types, e.g. `"WDJB-MJHT"`. */
|
|
33
|
+
readonly userCode: string;
|
|
34
|
+
/** URL the person opens to approve. */
|
|
35
|
+
readonly verificationUri: string;
|
|
36
|
+
/** Same URL with the code pre-filled, when the server provides it. Prefer it. */
|
|
37
|
+
readonly verificationUriComplete?: string;
|
|
38
|
+
/** Absolute epoch milliseconds after which `deviceCode` is dead. */
|
|
39
|
+
readonly expiresAt: number;
|
|
40
|
+
/** Minimum milliseconds between polls, as the server asked. Honour it. */
|
|
41
|
+
readonly intervalMs: number;
|
|
42
|
+
}
|
|
43
|
+
/** Arguments for starting a device flow. */
|
|
44
|
+
export interface StartDeviceFlowInput {
|
|
45
|
+
/** The registered doorkeeper application id. */
|
|
46
|
+
readonly clientId: string;
|
|
47
|
+
/** Space-separated scopes to request. */
|
|
48
|
+
readonly scope?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Arguments for polling a device flow to completion. */
|
|
51
|
+
export interface WaitForDeviceApprovalInput extends RequestOptions {
|
|
52
|
+
readonly clientId: string;
|
|
53
|
+
readonly deviceCode: string;
|
|
54
|
+
/** Starting poll interval. RFC 8628 `slow_down` raises it as the server asks. */
|
|
55
|
+
readonly intervalMs?: number;
|
|
56
|
+
/** Absolute epoch milliseconds to give up at. Defaults to the grant's own expiry. */
|
|
57
|
+
readonly expiresAt?: number;
|
|
58
|
+
/** Called on each poll so a host can keep a spinner honest. Never receives the code. */
|
|
59
|
+
readonly onPoll?: (state: "pending" | "slow_down") => void;
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* A device flow that ended for a reason the client cannot argue with.
|
|
63
|
+
*
|
|
64
|
+
* Branch on the subclasses, or on {@link OmsOAuthError.error} for the exact
|
|
65
|
+
* wire code. Never on the message.
|
|
66
|
+
*/
|
|
67
|
+
export declare class OmsDeviceFlowError extends OmsOAuthError {
|
|
68
|
+
/** Nothing about a terminal grant outcome improves by being tried again. */
|
|
69
|
+
get retryable(): boolean;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* The `device_code` outlived `expires_in` and the grant row is gone.
|
|
73
|
+
*
|
|
74
|
+
* The person took too long, or never opened the URL. Recover by calling
|
|
75
|
+
* {@link DeviceFlow.start} again for a fresh code.
|
|
76
|
+
*/
|
|
77
|
+
export declare class OmsDeviceExpiredError extends OmsDeviceFlowError {
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* The person refused, or the grant no longer exists.
|
|
81
|
+
*
|
|
82
|
+
* Both `access_denied` and `invalid_grant` land here on purpose. This backend
|
|
83
|
+
* represents a refusal by destroying the grant row, so the next poll finds
|
|
84
|
+
* nothing and answers `invalid_grant`; `access_denied` is handled identically
|
|
85
|
+
* so that emitting it later is not a breaking change. Read
|
|
86
|
+
* {@link OmsOAuthError.error} when the difference matters.
|
|
87
|
+
*/
|
|
88
|
+
export declare class OmsDeviceDeniedError extends OmsDeviceFlowError {
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* The device grant, exposed as `oms.auth.device`.
|
|
92
|
+
*
|
|
93
|
+
* Typical use:
|
|
94
|
+
* ```ts
|
|
95
|
+
* const grant = await oms.auth.device.start({ clientId });
|
|
96
|
+
* // host shows grant.verificationUriComplete ?? grant.verificationUri
|
|
97
|
+
* const tokens = await oms.auth.device.wait({ clientId, deviceCode: grant.deviceCode });
|
|
98
|
+
* ```
|
|
99
|
+
*
|
|
100
|
+
* Call it through a client with NO credential. Both endpoints authenticate
|
|
101
|
+
* with `client_id` in the body and want no `Authorization` header.
|
|
102
|
+
*/
|
|
103
|
+
export declare class DeviceFlow extends Resource {
|
|
104
|
+
/**
|
|
105
|
+
* `POST /oauth/authorize_device` - asks for a device code and a user code.
|
|
106
|
+
*
|
|
107
|
+
* `scope` is omitted from the request when the caller passes none, and the
|
|
108
|
+
* server then grants `openid` alone. An unknown scope is rejected here
|
|
109
|
+
* rather than at first use, so a typo fails fast.
|
|
110
|
+
*
|
|
111
|
+
* @throws {OmsDeviceFlowError} when the server named an OAuth error.
|
|
112
|
+
* `invalid_client` (HTTP 401) means the `clientId` is wrong: a build bug,
|
|
113
|
+
* not something the person can fix.
|
|
114
|
+
*/
|
|
115
|
+
start(input: StartDeviceFlowInput, options?: RequestOptions): Promise<DeviceAuthorization>;
|
|
116
|
+
/**
|
|
117
|
+
* Polls `POST /oauth/token` with `grant_type=urn:ietf:params:oauth:grant-type:device_code`
|
|
118
|
+
* until the person approves.
|
|
119
|
+
*
|
|
120
|
+
* Timing, in order:
|
|
121
|
+
* 1. sleep the interval BEFORE the first poll - the server would accept an
|
|
122
|
+
* immediate one, but the person has not opened a browser yet;
|
|
123
|
+
* 2. `authorization_pending` sleeps the interval and goes again;
|
|
124
|
+
* 3. `slow_down` adds {@link DEVICE_SLOW_DOWN_STEP_MS} to the interval
|
|
125
|
+
* permanently, then goes again;
|
|
126
|
+
* 4. HTTP 429 is rack-attack, not OAuth: wait what `Retry-After` said and go
|
|
127
|
+
* again, without touching the interval and without counting it against
|
|
128
|
+
* the flow;
|
|
129
|
+
* 5. a network fault or a 5xx waits one interval and goes again.
|
|
130
|
+
*
|
|
131
|
+
* The flow ends only at `expiresAt`, on a terminal OAuth error, or when the
|
|
132
|
+
* caller's `signal` aborts.
|
|
133
|
+
*
|
|
134
|
+
* @throws {OmsDeviceExpiredError} at `expiresAt`, and on `expired_token`.
|
|
135
|
+
* @throws {OmsDeviceDeniedError} when the person refused.
|
|
136
|
+
* @throws {OmsTimeoutError} with `code === "aborted"` when `signal` fired.
|
|
137
|
+
*/
|
|
138
|
+
wait(input: WaitForDeviceApprovalInput): Promise<TokenSet>;
|
|
139
|
+
/**
|
|
140
|
+
* One poll of the token endpoint. Returns the tokens once approved, or
|
|
141
|
+
* `null` while the person has not answered yet.
|
|
142
|
+
*
|
|
143
|
+
* Does not sleep and does not respect the interval: {@link wait} owns the
|
|
144
|
+
* timing. `slow_down` reads as `null` here, so a caller driving its own loop
|
|
145
|
+
* must raise its interval on its own - or use {@link wait}.
|
|
146
|
+
*
|
|
147
|
+
* @throws {OmsDeviceExpiredError} / {@link OmsDeviceDeniedError} / any other
|
|
148
|
+
* terminal OAuth failure.
|
|
149
|
+
*/
|
|
150
|
+
poll(input: {
|
|
151
|
+
clientId: string;
|
|
152
|
+
deviceCode: string;
|
|
153
|
+
}, options?: RequestOptions): Promise<TokenSet | null>;
|
|
154
|
+
/** The bare token request. Every caller above adds the policy around it. */
|
|
155
|
+
private exchange;
|
|
156
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `auth` namespace: signing in, refreshing, signing out, and knowing who
|
|
3
|
+
* the current credential belongs to.
|
|
4
|
+
*
|
|
5
|
+
* Re-exports every sibling module so nobody has to touch this file again.
|
|
6
|
+
*
|
|
7
|
+
* Two of these methods want a credential and the rest want none, which is the
|
|
8
|
+
* one thing to get right when wiring a host:
|
|
9
|
+
*
|
|
10
|
+
* - `device.start`, `device.wait`, `device.poll`, `refresh` and `revoke` carry
|
|
11
|
+
* `client_id` in a form body and must NOT send `Authorization`. Call them on
|
|
12
|
+
* an `Oms` built with no token.
|
|
13
|
+
* - `whoami` and `userinfo` are ordinary bearer-authenticated calls. Call them
|
|
14
|
+
* on the credentialed `Oms`.
|
|
15
|
+
*
|
|
16
|
+
* ```ts
|
|
17
|
+
* const anon = new Oms({ baseUrl, fetch });
|
|
18
|
+
* const grant = await anon.auth.device.start({ clientId, scope: "openid storage:read" });
|
|
19
|
+
* // host shows grant.verificationUriComplete ?? grant.verificationUri
|
|
20
|
+
* const set = await anon.auth.device.wait({ clientId, deviceCode: grant.deviceCode, ...grant });
|
|
21
|
+
*
|
|
22
|
+
* const tokens = new OAuthTokenProvider({
|
|
23
|
+
* store,
|
|
24
|
+
* refresh: (refreshToken) => anon.auth.refresh(refreshToken, { clientId }),
|
|
25
|
+
* });
|
|
26
|
+
* await tokens.set(set);
|
|
27
|
+
* const oms = new Oms({ baseUrl, fetch, tokens });
|
|
28
|
+
* const me = await oms.auth.whoami();
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
import { ApiClient, Resource } from "../http";
|
|
32
|
+
import type { RequestOptions } from "../types";
|
|
33
|
+
import { DeviceFlow } from "./device";
|
|
34
|
+
import type { IdentityClaims, TokenSet } from "./tokens";
|
|
35
|
+
export * from "./device";
|
|
36
|
+
export * from "./tokens";
|
|
37
|
+
/** Who the current credential belongs to. */
|
|
38
|
+
export interface WhoAmI {
|
|
39
|
+
/** `users.id`. The stable identifier; matches the OIDC `sub` claim. */
|
|
40
|
+
readonly id: string;
|
|
41
|
+
/** Current handle. Mutable - never key anything on it. */
|
|
42
|
+
readonly handle: string;
|
|
43
|
+
/** Current email. Mutable. */
|
|
44
|
+
readonly email?: string;
|
|
45
|
+
/** Scopes the credential carries, or `undefined` for a legacy session token. */
|
|
46
|
+
readonly scopes?: string[];
|
|
47
|
+
/** Whether the credential is an OAuth token rather than a legacy session token. */
|
|
48
|
+
readonly oauth: boolean;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* The subset of the OIDC discovery document worth naming. Everything else the
|
|
52
|
+
* server publishes is still there under the index signature.
|
|
53
|
+
*/
|
|
54
|
+
export interface DiscoveryDocument {
|
|
55
|
+
readonly issuer: string;
|
|
56
|
+
readonly token_endpoint: string;
|
|
57
|
+
readonly device_authorization_endpoint?: string;
|
|
58
|
+
readonly userinfo_endpoint?: string;
|
|
59
|
+
readonly jwks_uri?: string;
|
|
60
|
+
readonly scopes_supported?: string[];
|
|
61
|
+
readonly grant_types_supported?: string[];
|
|
62
|
+
readonly [member: string]: unknown;
|
|
63
|
+
}
|
|
64
|
+
/** The `auth` namespace, reachable as `oms.auth`. */
|
|
65
|
+
export declare class AuthNamespace extends Resource {
|
|
66
|
+
/** RFC 8628 device grant, for CLIs and headless clients. */
|
|
67
|
+
readonly device: DeviceFlow;
|
|
68
|
+
constructor(http: ApiClient);
|
|
69
|
+
/**
|
|
70
|
+
* `GET /account` with the current credential: resolves who is signed in.
|
|
71
|
+
*
|
|
72
|
+
* The cheapest way to check that a stored token is still alive. Needs the
|
|
73
|
+
* credential, so call it on the credentialed client.
|
|
74
|
+
*
|
|
75
|
+
* `email` comes back only when the `email` scope was granted, and `scopes` is
|
|
76
|
+
* `undefined` for a legacy opaque session token - which has no scopes because
|
|
77
|
+
* it carries full account authority.
|
|
78
|
+
*/
|
|
79
|
+
whoami(options?: RequestOptions): Promise<WhoAmI>;
|
|
80
|
+
/**
|
|
81
|
+
* Exchanges a refresh token for a fresh {@link TokenSet}.
|
|
82
|
+
*
|
|
83
|
+
* The answer carries a NEW refresh token and a freshly built id token;
|
|
84
|
+
* discard the old refresh token the moment this resolves. Not retried: a
|
|
85
|
+
* replay after a lost response would spend a refresh token the server has
|
|
86
|
+
* already rotated and sign the user out.
|
|
87
|
+
*
|
|
88
|
+
* A 4xx here is a normal end state, not a fault - the grant was revoked, or
|
|
89
|
+
* it sat unused past the server's absolute lifetime. Clear the stored set
|
|
90
|
+
* and ask the person to sign in again. {@link OAuthTokenProvider} does that
|
|
91
|
+
* on its own.
|
|
92
|
+
*/
|
|
93
|
+
refresh(refreshToken: string, input: {
|
|
94
|
+
clientId: string;
|
|
95
|
+
}, options?: RequestOptions): Promise<TokenSet>;
|
|
96
|
+
/**
|
|
97
|
+
* Revokes a token at `POST /oauth/revoke` (RFC 7009). Revoking a refresh
|
|
98
|
+
* token kills the whole grant.
|
|
99
|
+
*
|
|
100
|
+
* Answers `200` both for a real revocation and for a token the server has
|
|
101
|
+
* never seen, which is what RFC 7009 asks for: a client signing out must not
|
|
102
|
+
* be able to tell the two apart. A `403` means the token belongs to another
|
|
103
|
+
* application - a bug in the caller, not a state to recover from.
|
|
104
|
+
*/
|
|
105
|
+
revoke(token: string, input: {
|
|
106
|
+
clientId: string;
|
|
107
|
+
}, options?: RequestOptions): Promise<void>;
|
|
108
|
+
/**
|
|
109
|
+
* Reads the OIDC claims of the current credential from
|
|
110
|
+
* `GET /oauth/userinfo`. `sub` is `users.id`.
|
|
111
|
+
*
|
|
112
|
+
* Needs the `openid` scope; without it the answer is 403. Members whose
|
|
113
|
+
* value is null or empty are omitted from the response, so read defensively.
|
|
114
|
+
*
|
|
115
|
+
* `email_verified: false` means the server holds no proof for that address -
|
|
116
|
+
* NOT that the address is wrong. Do not present it as an invalid email.
|
|
117
|
+
*/
|
|
118
|
+
userinfo(options?: RequestOptions): Promise<IdentityClaims>;
|
|
119
|
+
/**
|
|
120
|
+
* `GET /.well-known/openid-configuration`, for diagnostics only.
|
|
121
|
+
*
|
|
122
|
+
* Never call this on the hot path. Every path this SDK uses is stable and
|
|
123
|
+
* same-origin with `baseUrl`; spending a round trip to rediscover them - in
|
|
124
|
+
* an isolate that may only ever handle one request - is pure waste.
|
|
125
|
+
*/
|
|
126
|
+
discover(options?: RequestOptions): Promise<DiscoveryDocument>;
|
|
127
|
+
}
|
|
@@ -0,0 +1,356 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Token providers: where the transport gets a bearer token from.
|
|
3
|
+
*
|
|
4
|
+
* The core has no storage. A provider is either a constant, or a thing the
|
|
5
|
+
* host wired to its own store (the CLI's config file, a Worker KV namespace, a
|
|
6
|
+
* browser's memory). Nothing here reads a file or an environment variable.
|
|
7
|
+
*
|
|
8
|
+
* Two credential shapes exist today and both are just bearer tokens on the
|
|
9
|
+
* wire:
|
|
10
|
+
* - a legacy opaque session token (a UUID minted by `Session`), which never
|
|
11
|
+
* expires and carries no scopes;
|
|
12
|
+
* - an OAuth 2 / OIDC access token from doorkeeper, which does expire and does
|
|
13
|
+
* carry scopes, and which comes with a refresh token.
|
|
14
|
+
*
|
|
15
|
+
* {@link TokenSet} models the second. The first is just a string.
|
|
16
|
+
*
|
|
17
|
+
* This module also owns the OAuth *wire* layer - the form encoding every
|
|
18
|
+
* `/oauth/*` endpoint wants, and the error shape they all answer with - because
|
|
19
|
+
* that is the same layer that turns a token response into a {@link TokenSet}.
|
|
20
|
+
* `device.ts` and `index.ts` are built on top of it.
|
|
21
|
+
*/
|
|
22
|
+
import { OmsError } from "../errors";
|
|
23
|
+
import { type ApiClient, type TokenProvider } from "../http";
|
|
24
|
+
import type { RequestOptions } from "../types";
|
|
25
|
+
/**
|
|
26
|
+
* An OAuth 2 token response, as doorkeeper returns it.
|
|
27
|
+
*
|
|
28
|
+
* `expiresAt` is absolute epoch milliseconds, not the `expires_in` seconds the
|
|
29
|
+
* server sends, so a stored set stays correct across a restart.
|
|
30
|
+
*/
|
|
31
|
+
export interface TokenSet {
|
|
32
|
+
/** Bearer token sent as `Authorization: Bearer <accessToken>`. */
|
|
33
|
+
readonly accessToken: string;
|
|
34
|
+
/** Refresh token, when the grant issued one. */
|
|
35
|
+
readonly refreshToken?: string;
|
|
36
|
+
/** OIDC identity token, when `openid` was among the scopes. */
|
|
37
|
+
readonly idToken?: string;
|
|
38
|
+
/** Always `"Bearer"` for this API. */
|
|
39
|
+
readonly tokenType: string;
|
|
40
|
+
/** Absolute expiry, epoch milliseconds. `undefined` means it does not expire. */
|
|
41
|
+
readonly expiresAt?: number;
|
|
42
|
+
/** Granted scopes, space-separated as the server sent them. */
|
|
43
|
+
readonly scope?: string;
|
|
44
|
+
}
|
|
45
|
+
/** Claims the SDK reads out of an OIDC id token. */
|
|
46
|
+
export interface IdentityClaims {
|
|
47
|
+
/**
|
|
48
|
+
* Stable user identifier: `users.id`. Never the handle and never the email,
|
|
49
|
+
* both of which the user can change.
|
|
50
|
+
*/
|
|
51
|
+
readonly sub: string;
|
|
52
|
+
readonly iss?: string;
|
|
53
|
+
readonly aud?: string | string[];
|
|
54
|
+
readonly exp?: number;
|
|
55
|
+
readonly iat?: number;
|
|
56
|
+
/** Present only when the `profile` scope was granted. Mutable, display only. */
|
|
57
|
+
readonly preferred_username?: string;
|
|
58
|
+
/** Present only when the `email` scope was granted. Mutable, display only. */
|
|
59
|
+
readonly email?: string;
|
|
60
|
+
readonly [claim: string]: unknown;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Persistence hook the host supplies so a refreshed token survives the
|
|
64
|
+
* process. Both methods may be async. The core never calls anything else.
|
|
65
|
+
*/
|
|
66
|
+
export interface TokenStore {
|
|
67
|
+
/** Loads the stored set, or `null` when the caller has never signed in. */
|
|
68
|
+
load(): TokenSet | null | Promise<TokenSet | null>;
|
|
69
|
+
/** Persists a set after a login or a refresh. */
|
|
70
|
+
save(tokens: TokenSet): void | Promise<void>;
|
|
71
|
+
/** Removes the stored set on logout or on an unrecoverable refresh failure. */
|
|
72
|
+
clear(): void | Promise<void>;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Every scope this authorization server knows about.
|
|
76
|
+
*
|
|
77
|
+
* An unknown scope is rejected at the device authorization request, so a typo
|
|
78
|
+
* fails fast at `start()` rather than silently at first use. Ask for the
|
|
79
|
+
* narrowest set the product actually uses: every extra scope makes the
|
|
80
|
+
* approval page scarier for no benefit.
|
|
81
|
+
*/
|
|
82
|
+
export declare const OMS_SCOPES: readonly ["openid", "profile", "email", "tools:read", "tools:write", "storage:read", "storage:write", "tickets:write"];
|
|
83
|
+
/** One of {@link OMS_SCOPES}. */
|
|
84
|
+
export type OmsScope = (typeof OMS_SCOPES)[number];
|
|
85
|
+
/** Refresh this long before the real expiry unless the host says otherwise. */
|
|
86
|
+
export declare const DEFAULT_REFRESH_SKEW_MS = 60000;
|
|
87
|
+
/**
|
|
88
|
+
* Wraps a constant token. This is what `new Oms({ token })` builds.
|
|
89
|
+
*
|
|
90
|
+
* Accepts both credential kinds: an opaque session UUID and a doorkeeper
|
|
91
|
+
* access token look identical on the wire.
|
|
92
|
+
*/
|
|
93
|
+
export declare function staticToken(token: string | null): TokenProvider;
|
|
94
|
+
/**
|
|
95
|
+
* Wraps a plain function as a provider, for a host that already has its own
|
|
96
|
+
* lookup and does not want to build an object.
|
|
97
|
+
*/
|
|
98
|
+
export declare function tokenFromFunction(fn: () => string | null | Promise<string | null>): TokenProvider;
|
|
99
|
+
/** Options for {@link OAuthTokenProvider} and {@link refreshingTokenProvider}. */
|
|
100
|
+
export interface RefreshingTokenOptions {
|
|
101
|
+
/**
|
|
102
|
+
* Where the set is read from and written back to. Omit it to keep the set in
|
|
103
|
+
* memory only, seeded from {@link RefreshingTokenOptions.tokens}.
|
|
104
|
+
*/
|
|
105
|
+
readonly store?: TokenStore;
|
|
106
|
+
/**
|
|
107
|
+
* Initial set, when there is no {@link TokenStore}. Mutually exclusive with
|
|
108
|
+
* `store`; passing both throws.
|
|
109
|
+
*/
|
|
110
|
+
readonly tokens?: TokenSet | null;
|
|
111
|
+
/**
|
|
112
|
+
* Exchanges a refresh token for a new set. Usually `auth.refresh` - but see
|
|
113
|
+
* the warning on {@link OAuthTokenProvider}: it must come from a client that
|
|
114
|
+
* does NOT carry this provider.
|
|
115
|
+
*/
|
|
116
|
+
readonly refresh: (refreshToken: string) => Promise<TokenSet>;
|
|
117
|
+
/**
|
|
118
|
+
* Refresh this many milliseconds before the real expiry, so a token does not
|
|
119
|
+
* die mid-flight. Defaults to {@link DEFAULT_REFRESH_SKEW_MS}.
|
|
120
|
+
*/
|
|
121
|
+
readonly skewMs?: number;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A {@link TokenProvider} that holds an OAuth {@link TokenSet} and renews it on
|
|
125
|
+
* its own, shortly before it expires.
|
|
126
|
+
*
|
|
127
|
+
* Concurrency is the whole point. Ten in-flight requests that all notice the
|
|
128
|
+
* same expiry share ONE refresh call: every caller awaits a single in-flight
|
|
129
|
+
* promise. Without that, nine of them burn a rotated refresh token and the
|
|
130
|
+
* user is signed out.
|
|
131
|
+
*
|
|
132
|
+
* ## Wire the `refresh` callback to a client WITHOUT this provider
|
|
133
|
+
*
|
|
134
|
+
* `POST /oauth/token` authenticates with `client_id` in the body and must not
|
|
135
|
+
* carry an `Authorization` header at all. More importantly, a refresh callback
|
|
136
|
+
* that talks through the same client this provider feeds is a deadlock: the
|
|
137
|
+
* refresh request asks the provider for a token, the provider is mid-refresh,
|
|
138
|
+
* and the two wait for each other forever.
|
|
139
|
+
*
|
|
140
|
+
* Build two clients - one anonymous for the OAuth endpoints, one credentialed
|
|
141
|
+
* for everything else:
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* const anon = new Oms({ baseUrl, fetch });
|
|
145
|
+
* const tokens = new OAuthTokenProvider({
|
|
146
|
+
* store,
|
|
147
|
+
* refresh: (refreshToken) => anon.auth.refresh(refreshToken, { clientId }),
|
|
148
|
+
* });
|
|
149
|
+
* const oms = new Oms({ baseUrl, fetch, tokens });
|
|
150
|
+
* ```
|
|
151
|
+
*
|
|
152
|
+
* Wiring it the other way is caught: the provider notices the re-entrant call
|
|
153
|
+
* and throws an {@link OmsError} explaining this, rather than hanging.
|
|
154
|
+
*/
|
|
155
|
+
export declare class OAuthTokenProvider implements TokenProvider {
|
|
156
|
+
private readonly store;
|
|
157
|
+
private readonly refreshFn;
|
|
158
|
+
private readonly skewMs;
|
|
159
|
+
/** Last set we know about. `undefined` means "never read the store yet". */
|
|
160
|
+
private cached;
|
|
161
|
+
/** The one refresh every concurrent caller waits on. */
|
|
162
|
+
private inFlight;
|
|
163
|
+
/** When the last successful refresh landed, epoch milliseconds. */
|
|
164
|
+
private lastRefreshAt;
|
|
165
|
+
/**
|
|
166
|
+
* True only for the synchronous instant in which the refresh callback is
|
|
167
|
+
* being invoked. A `getToken` that arrives inside that instant can only be
|
|
168
|
+
* the refresh request asking for a credential, which is the deadlock this
|
|
169
|
+
* class refuses to enter. Nothing else can observe it: no `await` runs
|
|
170
|
+
* between setting and clearing it.
|
|
171
|
+
*/
|
|
172
|
+
private invokingRefresh;
|
|
173
|
+
constructor(options: RefreshingTokenOptions);
|
|
174
|
+
/**
|
|
175
|
+
* The token the transport should send, refreshing first when the stored one
|
|
176
|
+
* is expired or about to be.
|
|
177
|
+
*
|
|
178
|
+
* Returns `null` when nobody is signed in. Returns the stored access token
|
|
179
|
+
* unchanged when there is no refresh token to renew it with: letting the API
|
|
180
|
+
* answer 401 says more than sending nothing at all.
|
|
181
|
+
*/
|
|
182
|
+
getToken(): Promise<string | null>;
|
|
183
|
+
/**
|
|
184
|
+
* The API rejected the token we just handed out. Renew once and tell the
|
|
185
|
+
* transport whether the retry is worth making.
|
|
186
|
+
*
|
|
187
|
+
* Never throws: a failed renewal returns `false` so the original
|
|
188
|
+
* `OmsAuthError` reaches the caller, which is the error that says "sign in
|
|
189
|
+
* again". The store is cleared on the way out.
|
|
190
|
+
*/
|
|
191
|
+
onUnauthorized(): Promise<boolean>;
|
|
192
|
+
/**
|
|
193
|
+
* The set as last seen, without touching the store or the network.
|
|
194
|
+
*
|
|
195
|
+
* `null` before the first {@link getToken} and after a {@link clear}. Read it
|
|
196
|
+
* for display (the granted scopes, the expiry); never to decide whether a
|
|
197
|
+
* request may go out.
|
|
198
|
+
*/
|
|
199
|
+
peek(): TokenSet | null;
|
|
200
|
+
/** Adopts a set - after a device flow completes - and persists it. */
|
|
201
|
+
set(tokens: TokenSet): Promise<void>;
|
|
202
|
+
/** Forgets the set here and in the store. Sign-out, or a dead grant. */
|
|
203
|
+
clear(): Promise<void>;
|
|
204
|
+
/**
|
|
205
|
+
* Reads the store, using the cached set while it is still good.
|
|
206
|
+
*
|
|
207
|
+
* Re-reading whenever the cached set is expired is what lets a second
|
|
208
|
+
* process pick up a refresh the first one already performed.
|
|
209
|
+
*/
|
|
210
|
+
private read;
|
|
211
|
+
/**
|
|
212
|
+
* The single in-flight refresh every concurrent caller shares.
|
|
213
|
+
*
|
|
214
|
+
* `force` skips the "somebody already refreshed" shortcut, for the one case
|
|
215
|
+
* where a live-looking set is known to be dead: the API answered 401 with it.
|
|
216
|
+
*/
|
|
217
|
+
private renew;
|
|
218
|
+
private performRefresh;
|
|
219
|
+
private forget;
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* Builds a provider that refreshes an expiring {@link TokenSet} on its own.
|
|
223
|
+
*
|
|
224
|
+
* Sugar over `new OAuthTokenProvider(options)`; read that class's notes before
|
|
225
|
+
* wiring the `refresh` callback.
|
|
226
|
+
*/
|
|
227
|
+
export declare function refreshingTokenProvider(options: RefreshingTokenOptions): OAuthTokenProvider;
|
|
228
|
+
/**
|
|
229
|
+
* Decodes an OIDC id token's claims WITHOUT verifying its signature.
|
|
230
|
+
*
|
|
231
|
+
* Only safe for reading `sub` out of a token this client just received over
|
|
232
|
+
* TLS from the issuer. Never use it to authorise anything.
|
|
233
|
+
*
|
|
234
|
+
* The device grant's id token carries no `nonce` - there is nowhere to put one
|
|
235
|
+
* in RFC 8628 - and it expires 120 seconds after issue. Read `sub` on arrival
|
|
236
|
+
* and never send it anywhere.
|
|
237
|
+
*
|
|
238
|
+
* @throws {OmsError} when the token is not three base64url segments, when the
|
|
239
|
+
* payload is not JSON, or when `sub` is missing. A token with no `sub` is
|
|
240
|
+
* useless: it is the only identifier that is safe to key anything on.
|
|
241
|
+
*/
|
|
242
|
+
export declare function decodeIdToken(idToken: string): IdentityClaims;
|
|
243
|
+
/**
|
|
244
|
+
* Whether a set is expired, or will be within `skewMs`.
|
|
245
|
+
*
|
|
246
|
+
* A set with no `expiresAt` never expires - that is how a legacy opaque
|
|
247
|
+
* session token behaves when it is carried in this shape.
|
|
248
|
+
*/
|
|
249
|
+
export declare function isExpired(tokens: TokenSet, skewMs?: number, now?: number): boolean;
|
|
250
|
+
/**
|
|
251
|
+
* Builds a {@link TokenSet} from a raw OAuth token endpoint response,
|
|
252
|
+
* converting `expires_in` seconds into an absolute `expiresAt`.
|
|
253
|
+
*
|
|
254
|
+
* This is the ONLY place that touches `expires_in`. `now` defaults to the
|
|
255
|
+
* moment of the call, which makes `expiresAt` slightly conservative - the
|
|
256
|
+
* server measured the lifetime from issue, and that is the right direction to
|
|
257
|
+
* be wrong in.
|
|
258
|
+
*
|
|
259
|
+
* @throws {OmsError} when the body carries no `access_token`.
|
|
260
|
+
*/
|
|
261
|
+
export declare function tokenSetFromResponse(body: unknown, now?: number): TokenSet;
|
|
262
|
+
/** The scopes a {@link TokenSet} carries, split out of its space-separated `scope`. */
|
|
263
|
+
export declare function scopesOf(tokens: TokenSet): string[];
|
|
264
|
+
/** Splits a scope value the server sent, in either shape, into a list. */
|
|
265
|
+
export declare function parseScopes(value: unknown): string[];
|
|
266
|
+
/**
|
|
267
|
+
* An in-memory {@link TokenStore}. Useful for tests and for a Worker that
|
|
268
|
+
* holds a token for the length of one request.
|
|
269
|
+
*/
|
|
270
|
+
export declare function memoryTokenStore(initial?: TokenSet | null): TokenStore;
|
|
271
|
+
/**
|
|
272
|
+
* An error the authorization server named itself, in the RFC 6749 §5.2 shape:
|
|
273
|
+
* `{ "error": "...", "error_description": "...", "error_uri": "..." }`.
|
|
274
|
+
*
|
|
275
|
+
* Branch on {@link OmsOAuthError.error}. NEVER on the message: the
|
|
276
|
+
* descriptions are I18n strings and they change.
|
|
277
|
+
*/
|
|
278
|
+
export declare class OmsOAuthError extends OmsError {
|
|
279
|
+
/** The OAuth error code, e.g. `"invalid_grant"`. The only field to branch on. */
|
|
280
|
+
readonly error: string;
|
|
281
|
+
/** The server's human sentence, when it sent one. Display only. */
|
|
282
|
+
readonly description: string | undefined;
|
|
283
|
+
/** `error_uri`, when the server sent one. */
|
|
284
|
+
readonly errorUri: string | undefined;
|
|
285
|
+
/** HTTP status it arrived with. `invalid_client` is 401; everything else 400. */
|
|
286
|
+
readonly status: number;
|
|
287
|
+
constructor(input: {
|
|
288
|
+
error: string;
|
|
289
|
+
description?: string;
|
|
290
|
+
errorUri?: string;
|
|
291
|
+
status: number;
|
|
292
|
+
method?: string;
|
|
293
|
+
url?: string;
|
|
294
|
+
cause?: unknown;
|
|
295
|
+
});
|
|
296
|
+
/**
|
|
297
|
+
* OAuth errors are decisions, not faults. Only the two the spec reserves for
|
|
298
|
+
* a struggling server are worth trying again.
|
|
299
|
+
*/
|
|
300
|
+
get retryable(): boolean;
|
|
301
|
+
toJSON(): Record<string, unknown>;
|
|
302
|
+
}
|
|
303
|
+
/**
|
|
304
|
+
* Recognises an OAuth error inside whatever the transport threw.
|
|
305
|
+
*
|
|
306
|
+
* Deliberately narrow: only 400 and 401 bodies are read as OAuth errors. A 429
|
|
307
|
+
* comes from rack-attack with the body `{"error":"rate_limited"}`, and that
|
|
308
|
+
* `error` key is NOT an OAuth code - reading it as one abandons a perfectly
|
|
309
|
+
* live device flow. It stays an {@link OmsQuotaError} and the caller handles
|
|
310
|
+
* it as a rate limit.
|
|
311
|
+
*
|
|
312
|
+
* @returns The typed error, or `undefined` when this was not an OAuth failure.
|
|
313
|
+
*/
|
|
314
|
+
export declare function oauthErrorFrom(thrown: unknown): OmsOAuthError | undefined;
|
|
315
|
+
/** What a 403 `insufficient_scope` challenge said was missing. */
|
|
316
|
+
export interface InsufficientScope {
|
|
317
|
+
/**
|
|
318
|
+
* The scopes the endpoint needed. EMPTY when the server named none, which
|
|
319
|
+
* means the endpoint has not been opened to OAuth clients at all - a backend
|
|
320
|
+
* gap, not a client bug. The two cases must read differently to the user.
|
|
321
|
+
*/
|
|
322
|
+
readonly required: string[];
|
|
323
|
+
/** The `realm` parameter, when present. */
|
|
324
|
+
readonly realm: string | undefined;
|
|
325
|
+
}
|
|
326
|
+
/**
|
|
327
|
+
* Reads the `WWW-Authenticate` challenge of a 403 that rejected an OAuth token
|
|
328
|
+
* for want of a scope.
|
|
329
|
+
*
|
|
330
|
+
* The fix for this error is a fresh authorization with a wider scope set, not
|
|
331
|
+
* a change to the account's permissions, and the message shown to the user
|
|
332
|
+
* should say so.
|
|
333
|
+
*
|
|
334
|
+
* Note for browser hosts: `WWW-Authenticate` is not a CORS-safelisted response
|
|
335
|
+
* header, so a cross-origin `fetch` will not expose it unless the server lists
|
|
336
|
+
* it in `Access-Control-Expose-Headers`. This returns `undefined` then.
|
|
337
|
+
*/
|
|
338
|
+
export declare function readInsufficientScope(error: unknown): InsufficientScope | undefined;
|
|
339
|
+
/**
|
|
340
|
+
* `POST`s an `application/x-www-form-urlencoded` body to an `/oauth/*` endpoint
|
|
341
|
+
* and returns the parsed JSON.
|
|
342
|
+
*
|
|
343
|
+
* The OAuth endpoints do not take JSON, which is why this exists next to
|
|
344
|
+
* `ApiClient.post` rather than using it. Blank values are dropped rather than
|
|
345
|
+
* sent empty, because doorkeeper reads `""` as a present-but-invalid parameter.
|
|
346
|
+
*
|
|
347
|
+
* Retries are OFF and stay off. Replaying `POST /oauth/token` after a lost
|
|
348
|
+
* response is not safe: the server may have rotated the refresh token already,
|
|
349
|
+
* and the replay would spend the old one and sign the user out. Callers that
|
|
350
|
+
* want to try again own the timing - `DeviceFlow.wait` does exactly that.
|
|
351
|
+
*
|
|
352
|
+
* These endpoints authenticate with `client_id` in the body and want no
|
|
353
|
+
* `Authorization` header. Call them through a client with no credential; see
|
|
354
|
+
* the note on {@link OAuthTokenProvider}.
|
|
355
|
+
*/
|
|
356
|
+
export declare function oauthPost(http: ApiClient, path: string, params: Record<string, string | undefined>, options?: RequestOptions): Promise<unknown>;
|