@atlasauth/js 0.5.0 → 0.7.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/CHANGELOG.md +188 -0
- package/dist/api-keys.d.ts +84 -0
- package/dist/api-keys.js +77 -0
- package/dist/api-keys.js.map +1 -0
- package/dist/captcha-controller.d.ts +95 -0
- package/dist/captcha-controller.js +205 -0
- package/dist/captcha-controller.js.map +1 -0
- package/dist/captcha-widget.d.ts +81 -0
- package/dist/captcha-widget.js +263 -0
- package/dist/captcha-widget.js.map +1 -0
- package/dist/config.d.ts +59 -0
- package/dist/config.js +146 -0
- package/dist/config.js.map +1 -0
- package/dist/frontchannel-logout.d.ts +7 -0
- package/dist/frontchannel-logout.js.map +1 -1
- package/dist/index.d.ts +7 -0
- package/dist/index.js +14 -0
- package/dist/index.js.map +1 -1
- package/dist/instance-settings.d.ts +58 -0
- package/dist/instance-settings.js +74 -0
- package/dist/instance-settings.js.map +1 -0
- package/dist/popup-channel.d.ts +95 -0
- package/dist/popup-channel.js +231 -0
- package/dist/popup-channel.js.map +1 -0
- package/dist/redirect.d.ts +53 -4
- package/dist/redirect.js +114 -9
- package/dist/redirect.js.map +1 -1
- package/dist/sign-out.d.ts +115 -0
- package/dist/sign-out.js +297 -0
- package/dist/sign-out.js.map +1 -0
- package/package.json +3 -2
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
# @atlasauth/js changelog
|
|
2
|
+
|
|
3
|
+
## 0.7.0
|
|
4
|
+
- **Popup OAuth completes when the opener page sends `Cross-Origin-Opener-Policy: same-origin`.** That
|
|
5
|
+
header puts the provider popup in another browsing-context group: `popup.closed` reads `true` at once and
|
|
6
|
+
the popup has no `window.opener`, so the result could never be posted back and the flow settled as a
|
|
7
|
+
cancel while the popup stayed open. Now the popup lands back on your page with the result in the URL
|
|
8
|
+
fragment (`#atlas_popup_result=…`, never the query string), the SDK there hands it to the opener over
|
|
9
|
+
`BroadcastChannel('atlas:oauth:<nonce>')` (a `localStorage` `storage` event where BroadcastChannel is
|
|
10
|
+
missing) and closes the popup.
|
|
11
|
+
- **Changed `startOauthPopup` cancel semantics.** `popup.closed` turning true within 1.5 s of opening means
|
|
12
|
+
the opener link was severed, not that the user closed it: the flow keeps waiting, probing `GET /v1/client`
|
|
13
|
+
with back-off, for up to 5 minutes, and settles as `popup_closed` only on that timeout, an explicit error
|
|
14
|
+
result, `signal` aborting, or a newer popup flow starting. A close after 1.5 s is a cancel after one final
|
|
15
|
+
session probe (as before).
|
|
16
|
+
- **Changed `startOauthPopup` request.** It now sends `popup_nonce` and `opener_origin`
|
|
17
|
+
(`window.location.origin`) with `mode: 'popup'`, and opens the popup with the window name `atlas:oauth`.
|
|
18
|
+
- **Added** to `startOauthPopup`: `input.signal?: AbortSignal`, `handlers.onSevered?: () => void`,
|
|
19
|
+
`PopupStartResult.severed?: boolean`; new `OAuthPopupInput` type; `POPUP_SEVERED_WINDOW_MS` (1500) and
|
|
20
|
+
`POPUP_SEVERED_TIMEOUT_MS` (300000); `mapOAuthPopupPayload(data)` (the payload half of
|
|
21
|
+
`parseOAuthPopupMessage`).
|
|
22
|
+
- **Added popup channel helpers:** `completeOAuthPopupLanding({ win?, ackTimeoutMs? })` →
|
|
23
|
+
`PopupLanding | null` (`{ payload, delivered }`; reads and scrubs the fragment, relays the result and
|
|
24
|
+
closes once the opener acknowledges — call it on startup), `closeOAuthPopupAfterSignIn({ win?, ackTimeoutMs? })`
|
|
25
|
+
(a page that finished a sign-in inside the Atlas popup closes itself when an opener is waiting),
|
|
26
|
+
`listenForPopupResult(nonce, { onResult, onSignedIn? })`, `readPopupLanding(hash)`, `popupChannelName(nonce)`,
|
|
27
|
+
`createPopupNonce()`, `PopupRelayMessage`, and the constants `OAUTH_POPUP_WINDOW_NAME`
|
|
28
|
+
(`'atlas:oauth'`), `POPUP_RESULT_FRAGMENT` (`'atlas_popup_result'`), `POPUP_SIGNAL_CHANNEL`.
|
|
29
|
+
- **Sign-out tombstones are shared by every tab.** `SessionTombstones` moved from `sessionStorage` to
|
|
30
|
+
`localStorage`, so after a failed revoke a newly opened tab no longer re-adopts the session. Entries are
|
|
31
|
+
`{ id, at }`, at most 20, kept 30 days, pruned on every read, and keyed per Frontend API origin
|
|
32
|
+
(`atlas:signed-out-sessions:<origin>`). `sessionStorage` is used only when `localStorage` throws; tombstones
|
|
33
|
+
an earlier version kept in `sessionStorage` are carried over.
|
|
34
|
+
- **Changed** `new SessionTombstones(options?)`: accepts `{ frontendApi?, storageKey?, now? }`
|
|
35
|
+
(`SessionTombstonesOptions`); a bare storage-key string is still accepted. **Added** `remove(...ids)` (forget
|
|
36
|
+
a tombstone once a later revoke is confirmed) and `subscribe(listener)` (another tab's changes, via the
|
|
37
|
+
`storage` event). `has()`/`list()` re-read storage, so other tabs' changes are seen.
|
|
38
|
+
|
|
39
|
+
## 0.6.0
|
|
40
|
+
- **Per-flow captcha widget rendering**, shared by the prebuilt sign-in / sign-up UIs:
|
|
41
|
+
`renderCaptchaWidget(flow, container)` → `{ token: Promise<string>, remove() }`,
|
|
42
|
+
`getCaptchaToken(flow, container)`, `loadCaptchaProvider(provider, siteKey, win)`,
|
|
43
|
+
`isRenderableCaptcha(provider)`, `flowNeedsCaptcha(flow)`, `isCaptchaError(error)` and
|
|
44
|
+
`CAPTCHA_ERROR_CODES`. Renders Turnstile, hCaptcha, reCAPTCHA v2, v3 and Enterprise, honouring the
|
|
45
|
+
`invisible` / `managed` / `visible` widget mode. A failed script load is retried on the next render.
|
|
46
|
+
- **Added types** `FlowCaptcha` (`{ enabled, provider?, siteKey?, widget? }` — one entry of
|
|
47
|
+
`/v1/appearance` `captchaFlows`), `CaptchaFlowKey`, `CaptchaWidgetMode`, `CaptchaWidgetHandle`.
|
|
48
|
+
- **Framework-agnostic captcha lifecycle** for the prebuilt forms of every framework SDK:
|
|
49
|
+
`createCaptchaController(options?)` → `CaptchaController` (`needed`, `configure(options)`,
|
|
50
|
+
`attach(el)`, `getToken()`, `reset()`, `destroy()`), with the new `CaptchaControllerOptions` type
|
|
51
|
+
(`{ flow, active, eager?, onSolved? }`); `localizeCaptchaErrors(errors, t)`.
|
|
52
|
+
- **Sign-in / sign-up submit helpers:** `signInCaptchaOptions(flow, state, onChallengeSolved)`,
|
|
53
|
+
`advanceWithCaptcha(client, state, values, { captcha?, identifier?, t })` → `{ state, identifier }`
|
|
54
|
+
(adds the captcha token on the identifier / `needs_captcha` step, reports a rejected token as
|
|
55
|
+
`CAPTCHA_FAILED` and draws a fresh widget), and `createSignUp(client, values, { consent, captcha?, t })`
|
|
56
|
+
(posts `/v1/client/sign_ups` with the sign-up captcha's token).
|
|
57
|
+
- **Instance settings:** `fetchInstanceSettings(client)` (`GET /v1/appearance`, null on failure),
|
|
58
|
+
`instanceSettingsFrom(body)`, `toSignUpInput(values, consent, captchaToken?)`, and the types
|
|
59
|
+
`InstanceSettings` (`{ captchaFlows, signUpFields, legal }`) and `PublicSignUpField` (moved here from
|
|
60
|
+
`@atlasauth/react`, which re-exports them unchanged).
|
|
61
|
+
|
|
62
|
+
- **New: `resolveFrontendApi(value, { component?, allowSameOrigin? })`** — validates a provider's
|
|
63
|
+
`frontendApi` and returns the request base URL. Throws an **`AtlasConfigError`** (new, exported;
|
|
64
|
+
`code: AtlasConfigErrorCode` = `'FRONTEND_API_REQUIRED' | 'FRONTEND_API_INVALID'`) when the value is
|
|
65
|
+
missing, non-string, blank, not an absolute `https:` URL (`http:` only for localhost / local development hosts), or
|
|
66
|
+
carries credentials, a query or a fragment. `"same-origin"` (new constant `SAME_ORIGIN_FRONTEND_API`)
|
|
67
|
+
resolves to `""` and a root-relative path such as `"/atlas"` is kept, for reverse proxies; both are refused
|
|
68
|
+
with `allowSameOrigin: false` (native apps). An explicit `""` is the **deprecated** same-origin spelling: it
|
|
69
|
+
still resolves to `""` (one-time console warning pointing at `"same-origin"`), and is refused with
|
|
70
|
+
`allowSameOrigin: false`. Every framework SDK now uses it instead of `frontendApi ?? ''`.
|
|
71
|
+
- **New: `revokeSession(request, sessionId, { retries = 1 })`** → `RevokeSessionOutcome`
|
|
72
|
+
(`{ revoked, status, response?, activeSessionId? }`). Retries once on a network failure, 5xx or 429; a 404
|
|
73
|
+
counts as revoked; `activeSessionId` is the parked session the server promoted.
|
|
74
|
+
- **New: `SessionTombstones`** — remembers (memory + `sessionStorage`, bounded to 20) the session ids a tab
|
|
75
|
+
signed out of, so a re-boot from a still-valid refresh cookie never re-adopts them.
|
|
76
|
+
- **New types:** `SignOutResult` (`{ revoked: boolean; activeSessionId?: string }`), `SignOutError`
|
|
77
|
+
(`code: 'SIGN_OUT_REVOKE_FAILED'`, `sessionId`, `status`), `ResolveFrontendApiOptions`, `RevokeSessionOutcome`.
|
|
78
|
+
- **`SignOutOptions.strict?: boolean`** added (reject with `SignOutError` when the revoke fails; honoured by
|
|
79
|
+
`@atlasauth/react`).
|
|
80
|
+
- **Popup OAuth:** when the popup closes without posting a result, `startOauthPopup()` checks
|
|
81
|
+
`GET /v1/client` once; if a session now exists it resolves `{ error: 'popup_closed', sessionDetected: true }`
|
|
82
|
+
(new `PopupStartResult.sessionDetected?: boolean`) so the caller re-boots instead of treating it as a cancel.
|
|
83
|
+
- **End-user API key self-service.** New `listApiKeys(client, { organizationId? })`,
|
|
84
|
+
`createApiKey(client, input)`, `updateApiKey(client, id, { name })` and
|
|
85
|
+
`revokeApiKey(client, id)` drive `/v1/client/me/api_keys` through a `FapiClient`.
|
|
86
|
+
Added types: `UserApiKey` (`id`, `subject_type`, `subject_id`, `name`, `prefix`, `claims`,
|
|
87
|
+
`scopes`, optional `limits`, `last_used_at`, `expires_at`, `revoked_at`, `created_at`),
|
|
88
|
+
`CreatedUserApiKey` (adds the one-time `secret`, `note`), `UserApiKeyList` (`data`,
|
|
89
|
+
`allowedScopes`, `allowedClaims`, `maxActiveKeys`, `activeCount`) and `CreateUserApiKeyInput`
|
|
90
|
+
(`name`, `expiresAt`, `expiresInSeconds`, `scopes`, `claims`, `organizationId`). A refused call
|
|
91
|
+
throws the new `ApiKeyRequestError` (`code`, `status`, `param`, `needsReverification`).
|
|
92
|
+
|
|
93
|
+
## 0.5.0
|
|
94
|
+
- **OIDC Front-Channel Logout helpers.** `runFrontChannelLogout(urls, { timeoutMs })` loads each
|
|
95
|
+
front-channel logout URL in a hidden, sandboxed iframe and resolves once they have all loaded or
|
|
96
|
+
after the timeout (default 5 s); it never throws, only loads `https:` URLs (and `http://localhost`
|
|
97
|
+
for development), and is a no-op without a DOM (SSR). `runFrontChannelLogoutFromResponse(res)`
|
|
98
|
+
reads `frontchannel_logout_urls` from a Frontend API sign-out response. The framework SDKs'
|
|
99
|
+
`signOut()` uses them; the new `SignOutOptions` type (`{ frontchannel?: boolean }`) is the option
|
|
100
|
+
they accept.
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
## 0.4.0
|
|
104
|
+
- `AttemptView` now carries `id` (`string | null`; `null` only on the pre-attempt captcha
|
|
105
|
+
challenge), plus optional `object`, `identifier` and `expires_at`, matching what the server
|
|
106
|
+
returns, so consumers can read `attempt.id` without a cast.
|
|
107
|
+
|
|
108
|
+
## 0.3.0
|
|
109
|
+
- **Captcha for any action.** New `captcha(client)` returns `config(action?)` (fetch the
|
|
110
|
+
centrally managed public provider + site key), `verify(token, action?)` (server-side check)
|
|
111
|
+
and `protect(element, action?)` (load the provider's widget script, render it, resolve the
|
|
112
|
+
solved token) — so an app can protect any action without hardcoding provider keys.
|
|
113
|
+
- New `CaptchaWidgetConfig` and `CaptchaVerification` types, plus `loadCaptchaScript()` /
|
|
114
|
+
`resetCaptchaScripts()`.
|
|
115
|
+
|
|
116
|
+
## 0.2.0
|
|
117
|
+
- **Native-session helpers** for apps without a cookie jar (React Native, Electron, extension
|
|
118
|
+
service workers): `exchangeForSession()` trades a first-party OAuth access token for an Atlas
|
|
119
|
+
session, and `refreshNativeSession()` rotates it without cookies. Both return `null` instead
|
|
120
|
+
of throwing on failure.
|
|
121
|
+
- `NativeSessionManager` holds the current session, hands out a fresh session JWT and fires
|
|
122
|
+
`onChange` on every rotation so the new refresh token can be persisted.
|
|
123
|
+
`serializeNativeSession()` / `parseNativeSession()` help store it. New `NativeSession` type.
|
|
124
|
+
|
|
125
|
+
## 0.1.9
|
|
126
|
+
- Fixed: the invisible Google overlay now uses a near-zero opacity instead of exactly `0`.
|
|
127
|
+
Google's button treats an element at `opacity: 0` as not visible and silently ignores the
|
|
128
|
+
click; the overlay stays invisible to users but now receives the click.
|
|
129
|
+
|
|
130
|
+
## 0.1.8
|
|
131
|
+
- Fixed: the native Google overlay now stretches Google's button iframe, which Google injects
|
|
132
|
+
asynchronously, to cover the whole wrapper (re-applied via a `MutationObserver`), so clicks
|
|
133
|
+
near the edges no longer miss.
|
|
134
|
+
|
|
135
|
+
## 0.1.7
|
|
136
|
+
- Added: `renderGoogleOverlayButton` accepts `visible` — `false` (default) keeps Google's button
|
|
137
|
+
as an invisible click target over your own styled button; `true` shows Google's own button.
|
|
138
|
+
- Added: `diagnoseProvider()` reports a provider's resolved mode, client id, overlay geometry and
|
|
139
|
+
whether a credential was minted this session, for debugging a dead Google button. New `Rect`,
|
|
140
|
+
`OverlayGeometry` and `ProviderDiagnosis` types; `resetGoogleCredentialFlag()`.
|
|
141
|
+
- Changed: the invisible Google overlay is stretched to fill its wrapper, and in development it
|
|
142
|
+
warns in the console when it is misaligned or undersized.
|
|
143
|
+
|
|
144
|
+
## 0.1.6
|
|
145
|
+
- Added: `reportProviderUnavailable(client, provider, reason)` sends a best-effort beacon when a
|
|
146
|
+
native provider (Google) can't be used, so the failure shows up in the dashboard. New
|
|
147
|
+
`ProviderUnavailableReason` type (`'script_blocked' | 'not_rendered'`).
|
|
148
|
+
- Changed: device telemetry honours Do-Not-Track — when it is on, no persistent device id is
|
|
149
|
+
stored. New `doNotTrackEnabled()`.
|
|
150
|
+
|
|
151
|
+
## 0.1.5
|
|
152
|
+
- Added: popup-mode social / OIDC sign-in. `startOauthPopup(client, { provider, redirectUrl })`
|
|
153
|
+
opens the provider in a popup, accepts the result only from the Frontend API origin, and falls
|
|
154
|
+
back to a full-page redirect when the popup is blocked. New `parseOAuthPopupMessage()`,
|
|
155
|
+
`PopupOutcome`, `PopupStartResult` and `OAuthPopupHandlers`.
|
|
156
|
+
- Added: `FapiClient.frontendApiOrigin`.
|
|
157
|
+
|
|
158
|
+
## 0.1.4
|
|
159
|
+
- Added: `startOauthRedirect(client, { provider, redirectUrl })` starts a redirect OAuth sign-in
|
|
160
|
+
and returns the provider's authorization URL.
|
|
161
|
+
|
|
162
|
+
## 0.1.3
|
|
163
|
+
- Added: `renderGoogleOverlayButton` accepts `oneTap` to also show Google One Tap (off by
|
|
164
|
+
default).
|
|
165
|
+
|
|
166
|
+
## 0.1.2
|
|
167
|
+
- Added: native Google sign-in with replay protection. `mintNativeNonce(client, provider)` gets a
|
|
168
|
+
single-use nonce from Atlas and `nativeSignIn(client, { provider, idToken, nonce })` signs in
|
|
169
|
+
with a provider id token, returning the same `FlowState` as the other sign-in flows.
|
|
170
|
+
|
|
171
|
+
## 0.1.1
|
|
172
|
+
- First published release.
|
|
173
|
+
- Sign-in / sign-up flow driver over the Frontend API (`FapiClient`, `initialFlow`, `advance`,
|
|
174
|
+
`startSignUp`, `prepareFactor`, `pollAttempt`) with attempt helpers (`nextStep`, `fieldErrors`,
|
|
175
|
+
`formErrors`).
|
|
176
|
+
- Session token cache with refresh scheduling (`TokenCache`) and cross-tab refresh election
|
|
177
|
+
(`winsElection`, `bestChannel`).
|
|
178
|
+
- Google Identity Services helpers (`loadGoogleIdentityServices`, `renderGoogleButton`,
|
|
179
|
+
`renderGoogleOverlayButton`), passkeys (`createPasskey`, `getPasskeyAssertion`,
|
|
180
|
+
`passkeysSupported`), Telegram and Sign-In with Ethereum request builders.
|
|
181
|
+
- Redirect handling (`readRedirectResult`, `stripRedirectParams`), password reset
|
|
182
|
+
(`startPasswordReset`, `advancePasswordReset`), step-up re-authentication
|
|
183
|
+
(`reauthenticate`), connecting an extra provider (`connectExternalAccount`) and provider
|
|
184
|
+
tokens (`getProviderToken`).
|
|
185
|
+
- Silent cross-site session check (`checkSession`, `handleSilentCallback`) and device
|
|
186
|
+
telemetry (`sendTelemetry`).
|
|
187
|
+
- Authorization helpers re-exported from `@atlasauth/authz` (`hasFromClaims`,
|
|
188
|
+
`protectFromClaims`, `evaluate`, `ForbiddenError`, …).
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import type { FapiClient } from './fapi';
|
|
2
|
+
/**
|
|
3
|
+
* End-user API key self-service — a signed-in user manages their OWN `ak_` keys
|
|
4
|
+
* (or, when the application allows it, their active organization's) over
|
|
5
|
+
* `/v1/client/me/api_keys`. Cookie-authenticated through the {@link FapiClient};
|
|
6
|
+
* the feature is off until the application enables it.
|
|
7
|
+
*
|
|
8
|
+
* Creating a key needs a RECENT re-authentication: a stale session gets an
|
|
9
|
+
* {@link ApiKeyRequestError} whose `code` is `STEP_UP_REQUIRED`. Re-authenticate
|
|
10
|
+
* (`reauthenticate()`), then call `createApiKey` again.
|
|
11
|
+
*/
|
|
12
|
+
/** An end-user API key as its owner sees it. The secret is never part of it. */
|
|
13
|
+
export interface UserApiKey {
|
|
14
|
+
object: 'api_key';
|
|
15
|
+
id: string;
|
|
16
|
+
subject_type: 'user' | 'organization';
|
|
17
|
+
subject_id: string;
|
|
18
|
+
name: string | null;
|
|
19
|
+
/** The public `ak_…` head — recognises a key, cannot be used as one. */
|
|
20
|
+
prefix: string;
|
|
21
|
+
claims: Record<string, unknown>;
|
|
22
|
+
scopes: string[] | null;
|
|
23
|
+
/** Usage limits on the key, when the application sets them. */
|
|
24
|
+
limits?: Record<string, unknown> | null;
|
|
25
|
+
last_used_at: number | null;
|
|
26
|
+
expires_at: number | null;
|
|
27
|
+
revoked_at: number | null;
|
|
28
|
+
created_at: number;
|
|
29
|
+
}
|
|
30
|
+
/** The create response: the key plus its one-time `secret`. */
|
|
31
|
+
export interface CreatedUserApiKey extends UserApiKey {
|
|
32
|
+
/** The plaintext `ak_…` key. Shown exactly once — store it now. */
|
|
33
|
+
secret: string;
|
|
34
|
+
note?: string;
|
|
35
|
+
}
|
|
36
|
+
export interface UserApiKeyList {
|
|
37
|
+
data: UserApiKey[];
|
|
38
|
+
/** The scopes a new key may carry. */
|
|
39
|
+
allowedScopes: string[];
|
|
40
|
+
/** The claim names a new key may set. */
|
|
41
|
+
allowedClaims: string[];
|
|
42
|
+
/** The most active keys this user (or organization) may hold. */
|
|
43
|
+
maxActiveKeys: number;
|
|
44
|
+
/** Active (unrevoked, unexpired) keys held now. */
|
|
45
|
+
activeCount: number;
|
|
46
|
+
}
|
|
47
|
+
export interface CreateUserApiKeyInput {
|
|
48
|
+
name?: string;
|
|
49
|
+
/** Absolute expiry, epoch milliseconds. Not with `expiresInSeconds`. */
|
|
50
|
+
expiresAt?: number;
|
|
51
|
+
/** Relative lifetime in seconds. Not with `expiresAt`. */
|
|
52
|
+
expiresInSeconds?: number;
|
|
53
|
+
/** Only scopes the application allows (`allowedScopes`). */
|
|
54
|
+
scopes?: string[];
|
|
55
|
+
/** Only claim names the application allows (`allowedClaims`). */
|
|
56
|
+
claims?: Record<string, unknown>;
|
|
57
|
+
/** Create the key for this (active) organization instead of the user. */
|
|
58
|
+
organizationId?: string;
|
|
59
|
+
}
|
|
60
|
+
/** A refused request. `code` is the server's stable error code. */
|
|
61
|
+
export declare class ApiKeyRequestError extends Error {
|
|
62
|
+
readonly code: string;
|
|
63
|
+
readonly status: number;
|
|
64
|
+
readonly param?: string;
|
|
65
|
+
constructor(status: number, code: string, message: string, param?: string);
|
|
66
|
+
/** True when the action needs a fresh re-authentication first. */
|
|
67
|
+
get needsReverification(): boolean;
|
|
68
|
+
}
|
|
69
|
+
/** List the user's keys (or the active organization's, with `organizationId`). */
|
|
70
|
+
export declare function listApiKeys(client: FapiClient, options?: {
|
|
71
|
+
organizationId?: string;
|
|
72
|
+
}): Promise<UserApiKeyList>;
|
|
73
|
+
/** Create a key. The returned `secret` is shown exactly once. */
|
|
74
|
+
export declare function createApiKey(client: FapiClient, input?: CreateUserApiKeyInput): Promise<CreatedUserApiKey>;
|
|
75
|
+
/** Rename a key (`null` clears the name). */
|
|
76
|
+
export declare function updateApiKey(client: FapiClient, id: string, input: {
|
|
77
|
+
name: string | null;
|
|
78
|
+
}): Promise<UserApiKey>;
|
|
79
|
+
/** Revoke a key. Immediate and permanent. */
|
|
80
|
+
export declare function revokeApiKey(client: FapiClient, id: string): Promise<{
|
|
81
|
+
object: 'api_key';
|
|
82
|
+
id: string;
|
|
83
|
+
revoked: true;
|
|
84
|
+
}>;
|
package/dist/api-keys.js
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiKeyRequestError = void 0;
|
|
4
|
+
exports.listApiKeys = listApiKeys;
|
|
5
|
+
exports.createApiKey = createApiKey;
|
|
6
|
+
exports.updateApiKey = updateApiKey;
|
|
7
|
+
exports.revokeApiKey = revokeApiKey;
|
|
8
|
+
/** A refused request. `code` is the server's stable error code. */
|
|
9
|
+
class ApiKeyRequestError extends Error {
|
|
10
|
+
code;
|
|
11
|
+
status;
|
|
12
|
+
param;
|
|
13
|
+
constructor(status, code, message, param) {
|
|
14
|
+
super(message);
|
|
15
|
+
this.name = 'ApiKeyRequestError';
|
|
16
|
+
this.status = status;
|
|
17
|
+
this.code = code;
|
|
18
|
+
this.param = param;
|
|
19
|
+
}
|
|
20
|
+
/** True when the action needs a fresh re-authentication first. */
|
|
21
|
+
get needsReverification() {
|
|
22
|
+
return this.code === 'STEP_UP_REQUIRED';
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
exports.ApiKeyRequestError = ApiKeyRequestError;
|
|
26
|
+
const BASE = '/v1/client/me/api_keys';
|
|
27
|
+
async function call(client, path, init = {}) {
|
|
28
|
+
const res = await client.request(path, init);
|
|
29
|
+
if (!res.ok || !res.data) {
|
|
30
|
+
const first = res.errors[0];
|
|
31
|
+
throw new ApiKeyRequestError(res.status, first?.code ?? 'UNKNOWN', first?.message ?? 'The request failed.', first?.param);
|
|
32
|
+
}
|
|
33
|
+
return res.data;
|
|
34
|
+
}
|
|
35
|
+
/** List the user's keys (or the active organization's, with `organizationId`). */
|
|
36
|
+
async function listApiKeys(client, options = {}) {
|
|
37
|
+
const query = options.organizationId
|
|
38
|
+
? `?organization_id=${encodeURIComponent(options.organizationId)}`
|
|
39
|
+
: '';
|
|
40
|
+
const body = await call(client, `${BASE}${query}`, { method: 'GET' });
|
|
41
|
+
return {
|
|
42
|
+
data: body.data ?? [],
|
|
43
|
+
allowedScopes: body.allowed_scopes ?? [],
|
|
44
|
+
allowedClaims: body.allowed_claims ?? [],
|
|
45
|
+
maxActiveKeys: body.max_active_keys ?? 0,
|
|
46
|
+
activeCount: body.active_count ?? 0,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
/** Create a key. The returned `secret` is shown exactly once. */
|
|
50
|
+
async function createApiKey(client, input = {}) {
|
|
51
|
+
const body = {};
|
|
52
|
+
if (input.name !== undefined)
|
|
53
|
+
body.name = input.name;
|
|
54
|
+
if (input.expiresAt !== undefined)
|
|
55
|
+
body.expires_at = input.expiresAt;
|
|
56
|
+
if (input.expiresInSeconds !== undefined)
|
|
57
|
+
body.expires_in_seconds = input.expiresInSeconds;
|
|
58
|
+
if (input.scopes !== undefined)
|
|
59
|
+
body.scopes = input.scopes;
|
|
60
|
+
if (input.claims !== undefined)
|
|
61
|
+
body.claims = input.claims;
|
|
62
|
+
if (input.organizationId !== undefined)
|
|
63
|
+
body.organization_id = input.organizationId;
|
|
64
|
+
return call(client, BASE, { method: 'POST', body: JSON.stringify(body) });
|
|
65
|
+
}
|
|
66
|
+
/** Rename a key (`null` clears the name). */
|
|
67
|
+
async function updateApiKey(client, id, input) {
|
|
68
|
+
return call(client, `${BASE}/${encodeURIComponent(id)}`, {
|
|
69
|
+
method: 'PATCH',
|
|
70
|
+
body: JSON.stringify({ name: input.name }),
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
/** Revoke a key. Immediate and permanent. */
|
|
74
|
+
async function revokeApiKey(client, id) {
|
|
75
|
+
return call(client, `${BASE}/${encodeURIComponent(id)}/revoke`, { method: 'POST' });
|
|
76
|
+
}
|
|
77
|
+
//# sourceMappingURL=api-keys.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"api-keys.js","sourceRoot":"","sources":["../src/api-keys.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { type FieldError } from './attempt';
|
|
2
|
+
import { type FapiClient, type FlowState } from './fapi';
|
|
3
|
+
import { type FlowCaptcha } from './captcha-widget';
|
|
4
|
+
/**
|
|
5
|
+
* The captcha half of a prebuilt sign-in / sign-up form, without a framework.
|
|
6
|
+
*
|
|
7
|
+
* The Vue, Svelte, Angular, Solid and Elements bindings each hold one of these
|
|
8
|
+
* per form; React's `useCaptcha` hook is the same lifecycle. It is driven by the
|
|
9
|
+
* instance's settings (`captchaFlows` from `/v1/appearance`), so switching
|
|
10
|
+
* captcha on in the dashboard makes the forms render it with no code change.
|
|
11
|
+
*
|
|
12
|
+
* Lifecycle: a visible/managed widget is drawn as soon as its slot is attached,
|
|
13
|
+
* so the user can solve it while filling in the form; an invisible one is
|
|
14
|
+
* executed only when the form is submitted ({@link CaptchaController.getToken}).
|
|
15
|
+
* Tokens are single-use, so after a submit spent one the caller
|
|
16
|
+
* {@link CaptchaController.reset}s, which draws a fresh widget.
|
|
17
|
+
*/
|
|
18
|
+
export interface CaptchaControllerOptions {
|
|
19
|
+
/** The flow's captcha settings (`captchaFlows.signUp`, …). */
|
|
20
|
+
flow: FlowCaptcha | null | undefined;
|
|
21
|
+
/** Render at all — the caller's "this flow requires a token here" decision. */
|
|
22
|
+
active: boolean;
|
|
23
|
+
/** Draw the widget as soon as the slot is attached. Default: unless invisible. */
|
|
24
|
+
eager?: boolean;
|
|
25
|
+
/** Called with the token when an eagerly drawn widget is solved. */
|
|
26
|
+
onSolved?: (token: string) => void;
|
|
27
|
+
}
|
|
28
|
+
export interface CaptchaController {
|
|
29
|
+
/** Whether the current settings render a widget at all. */
|
|
30
|
+
readonly needed: boolean;
|
|
31
|
+
/** Replace the settings; redraws an attached widget only when they changed. */
|
|
32
|
+
configure(options: CaptchaControllerOptions): void;
|
|
33
|
+
/** Attach the element the widget draws into (null detaches and removes it). */
|
|
34
|
+
attach(el: HTMLElement | null): void;
|
|
35
|
+
/**
|
|
36
|
+
* The solved token, rendering/executing the widget first if needed. Null when
|
|
37
|
+
* no captcha is needed, or the provider failed (the server then refuses with
|
|
38
|
+
* CAPTCHA_REQUIRED, which the caller turns into a message).
|
|
39
|
+
*/
|
|
40
|
+
getToken(): Promise<string | null>;
|
|
41
|
+
/** Drop the (spent) widget and, for an eager one, draw a fresh one. */
|
|
42
|
+
reset(): void;
|
|
43
|
+
/** Remove the widget for good (component teardown). */
|
|
44
|
+
destroy(): void;
|
|
45
|
+
}
|
|
46
|
+
export declare function createCaptchaController(initial?: CaptchaControllerOptions): CaptchaController;
|
|
47
|
+
/**
|
|
48
|
+
* Swap the server's captcha refusals (CAPTCHA_REQUIRED / CAPTCHA_FAILED, which
|
|
49
|
+
* the server ties to the `captcha_token` param) for a localized, form-level
|
|
50
|
+
* message — there is no `captcha_token` input to hang a field error on. `t`
|
|
51
|
+
* resolves the `captcha.required` / `captcha.failed` catalog keys.
|
|
52
|
+
*/
|
|
53
|
+
export declare function localizeCaptchaErrors(errors: FieldError[], t: (key: string) => string): {
|
|
54
|
+
errors: FieldError[];
|
|
55
|
+
captchaRejected: boolean;
|
|
56
|
+
};
|
|
57
|
+
/**
|
|
58
|
+
* The sign-in captcha settings for the flow's current step: drawn on the
|
|
59
|
+
* identifier step when the instance ALWAYS requires one for sign-in (so the
|
|
60
|
+
* first POST already carries the token), and on the `needs_captcha` challenge
|
|
61
|
+
* step the server answers with when it withheld the attempt (always-on, or
|
|
62
|
+
* risk-triggered) — where it is drawn at once (even an invisible one) and
|
|
63
|
+
* solving it calls `onChallengeSolved`, which should resubmit.
|
|
64
|
+
*/
|
|
65
|
+
export declare function signInCaptchaOptions(flow: FlowCaptcha | null | undefined, state: FlowState, onChallengeSolved: () => void): CaptchaControllerOptions;
|
|
66
|
+
/**
|
|
67
|
+
* {@link advance} with the sign-in captcha folded in — what a prebuilt sign-in
|
|
68
|
+
* form calls on submit. On the identifier / `needs_captcha` step it adds the
|
|
69
|
+
* widget's token (re-issuing the identifier POST the challenge marker withheld);
|
|
70
|
+
* a token the provider rejected comes back as the same challenge, so it reports
|
|
71
|
+
* CAPTCHA_FAILED and draws a fresh widget (tokens are single-use). Captcha
|
|
72
|
+
* refusals are localized through `t`.
|
|
73
|
+
*
|
|
74
|
+
* `identifier` is the last identifier the user typed (the challenge marker
|
|
75
|
+
* carries none to re-send); the returned one is what to keep for next time.
|
|
76
|
+
*/
|
|
77
|
+
export declare function advanceWithCaptcha(client: FapiClient, state: FlowState, values: Record<string, string>, opts: {
|
|
78
|
+
captcha?: CaptchaController | null;
|
|
79
|
+
identifier?: string;
|
|
80
|
+
t: (key: string) => string;
|
|
81
|
+
}): Promise<{
|
|
82
|
+
state: FlowState;
|
|
83
|
+
identifier: string;
|
|
84
|
+
}>;
|
|
85
|
+
/**
|
|
86
|
+
* Create the account from a prebuilt create-account form: takes the sign-up
|
|
87
|
+
* captcha's token (when one is needed), POSTs `/v1/client/sign_ups` via
|
|
88
|
+
* {@link startSignUp}, localizes captcha refusals and, when the create was
|
|
89
|
+
* refused, draws a fresh widget (the token was spent either way).
|
|
90
|
+
*/
|
|
91
|
+
export declare function createSignUp(client: FapiClient, values: Record<string, string>, opts: {
|
|
92
|
+
consent: boolean;
|
|
93
|
+
captcha?: CaptchaController | null;
|
|
94
|
+
t: (key: string) => string;
|
|
95
|
+
}): Promise<FlowState>;
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.createCaptchaController = createCaptchaController;
|
|
4
|
+
exports.localizeCaptchaErrors = localizeCaptchaErrors;
|
|
5
|
+
exports.signInCaptchaOptions = signInCaptchaOptions;
|
|
6
|
+
exports.advanceWithCaptcha = advanceWithCaptcha;
|
|
7
|
+
exports.createSignUp = createSignUp;
|
|
8
|
+
const attempt_1 = require("./attempt");
|
|
9
|
+
const fapi_1 = require("./fapi");
|
|
10
|
+
const instance_settings_1 = require("./instance-settings");
|
|
11
|
+
const captcha_widget_1 = require("./captcha-widget");
|
|
12
|
+
function createCaptchaController(initial) {
|
|
13
|
+
let options = initial ?? { flow: undefined, active: false };
|
|
14
|
+
let container = null;
|
|
15
|
+
let handle = null;
|
|
16
|
+
// The handle whose token a submit already took: the next getToken() draws a
|
|
17
|
+
// fresh widget instead of replaying it.
|
|
18
|
+
let spent = null;
|
|
19
|
+
// The settings the current widget was drawn with, so a redraw happens only
|
|
20
|
+
// when they actually change.
|
|
21
|
+
let drawnKey = null;
|
|
22
|
+
const needed = () => options.active && !!options.flow && (0, captcha_widget_1.isRenderableCaptcha)(options.flow.provider);
|
|
23
|
+
const eager = () => options.eager ?? options.flow?.widget !== 'invisible';
|
|
24
|
+
const key = () => {
|
|
25
|
+
const f = options.flow;
|
|
26
|
+
return `${needed()}|${f?.provider ?? ''}|${f?.siteKey ?? ''}|${f?.widget ?? ''}|${eager()}`;
|
|
27
|
+
};
|
|
28
|
+
const remove = () => {
|
|
29
|
+
handle?.remove();
|
|
30
|
+
handle = null;
|
|
31
|
+
drawnKey = null;
|
|
32
|
+
};
|
|
33
|
+
const mount = () => {
|
|
34
|
+
remove();
|
|
35
|
+
const flow = options.flow;
|
|
36
|
+
if (!needed() || !flow || !container)
|
|
37
|
+
return null;
|
|
38
|
+
const h = (0, captcha_widget_1.renderCaptchaWidget)(flow, container);
|
|
39
|
+
handle = h;
|
|
40
|
+
drawnKey = key();
|
|
41
|
+
// Swallow here so a removed / failed widget is never an unhandled
|
|
42
|
+
// rejection; getToken() reads the same promise and reports it.
|
|
43
|
+
h.token.then((token) => {
|
|
44
|
+
if (handle === h)
|
|
45
|
+
options.onSolved?.(token);
|
|
46
|
+
}, () => undefined);
|
|
47
|
+
return h;
|
|
48
|
+
};
|
|
49
|
+
const reset = () => {
|
|
50
|
+
remove();
|
|
51
|
+
if (needed() && eager())
|
|
52
|
+
mount();
|
|
53
|
+
};
|
|
54
|
+
return {
|
|
55
|
+
get needed() {
|
|
56
|
+
return needed();
|
|
57
|
+
},
|
|
58
|
+
configure(next) {
|
|
59
|
+
options = next;
|
|
60
|
+
if (!container)
|
|
61
|
+
return;
|
|
62
|
+
if (!needed() || !eager())
|
|
63
|
+
remove();
|
|
64
|
+
else if (drawnKey !== key())
|
|
65
|
+
mount();
|
|
66
|
+
},
|
|
67
|
+
attach(el) {
|
|
68
|
+
if (el === container)
|
|
69
|
+
return;
|
|
70
|
+
remove();
|
|
71
|
+
container = el;
|
|
72
|
+
if (el && needed() && eager())
|
|
73
|
+
mount();
|
|
74
|
+
},
|
|
75
|
+
async getToken() {
|
|
76
|
+
if (!needed())
|
|
77
|
+
return null;
|
|
78
|
+
let h = handle;
|
|
79
|
+
if (!h || spent === h)
|
|
80
|
+
h = mount();
|
|
81
|
+
if (!h)
|
|
82
|
+
return null;
|
|
83
|
+
try {
|
|
84
|
+
const token = await h.token;
|
|
85
|
+
spent = h;
|
|
86
|
+
return token;
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
// The provider errored / timed out: start over with a fresh widget.
|
|
90
|
+
if (handle === h)
|
|
91
|
+
reset();
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
},
|
|
95
|
+
reset,
|
|
96
|
+
destroy() {
|
|
97
|
+
remove();
|
|
98
|
+
container = null;
|
|
99
|
+
},
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Swap the server's captcha refusals (CAPTCHA_REQUIRED / CAPTCHA_FAILED, which
|
|
104
|
+
* the server ties to the `captcha_token` param) for a localized, form-level
|
|
105
|
+
* message — there is no `captcha_token` input to hang a field error on. `t`
|
|
106
|
+
* resolves the `captcha.required` / `captcha.failed` catalog keys.
|
|
107
|
+
*/
|
|
108
|
+
function localizeCaptchaErrors(errors, t) {
|
|
109
|
+
let captchaRejected = false;
|
|
110
|
+
const out = errors.map((error) => {
|
|
111
|
+
if (!(0, captcha_widget_1.isCaptchaError)(error))
|
|
112
|
+
return error;
|
|
113
|
+
captchaRejected = true;
|
|
114
|
+
return {
|
|
115
|
+
code: error.code,
|
|
116
|
+
message: t(error.code === 'CAPTCHA_REQUIRED' ? 'captcha.required' : 'captcha.failed'),
|
|
117
|
+
};
|
|
118
|
+
});
|
|
119
|
+
return { errors: out, captchaRejected };
|
|
120
|
+
}
|
|
121
|
+
/**
|
|
122
|
+
* The sign-in captcha settings for the flow's current step: drawn on the
|
|
123
|
+
* identifier step when the instance ALWAYS requires one for sign-in (so the
|
|
124
|
+
* first POST already carries the token), and on the `needs_captcha` challenge
|
|
125
|
+
* step the server answers with when it withheld the attempt (always-on, or
|
|
126
|
+
* risk-triggered) — where it is drawn at once (even an invisible one) and
|
|
127
|
+
* solving it calls `onChallengeSolved`, which should resubmit.
|
|
128
|
+
*/
|
|
129
|
+
function signInCaptchaOptions(flow, state, onChallengeSolved) {
|
|
130
|
+
const kind = state.attempt ? (0, attempt_1.nextStep)(state.attempt).kind : 'collect_identifier';
|
|
131
|
+
const challenged = kind === 'collect_captcha';
|
|
132
|
+
return {
|
|
133
|
+
flow,
|
|
134
|
+
active: challenged || (kind === 'collect_identifier' && (0, captcha_widget_1.flowNeedsCaptcha)(flow)),
|
|
135
|
+
...(challenged ? { eager: true, onSolved: () => onChallengeSolved() } : {}),
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
/**
|
|
139
|
+
* {@link advance} with the sign-in captcha folded in — what a prebuilt sign-in
|
|
140
|
+
* form calls on submit. On the identifier / `needs_captcha` step it adds the
|
|
141
|
+
* widget's token (re-issuing the identifier POST the challenge marker withheld);
|
|
142
|
+
* a token the provider rejected comes back as the same challenge, so it reports
|
|
143
|
+
* CAPTCHA_FAILED and draws a fresh widget (tokens are single-use). Captcha
|
|
144
|
+
* refusals are localized through `t`.
|
|
145
|
+
*
|
|
146
|
+
* `identifier` is the last identifier the user typed (the challenge marker
|
|
147
|
+
* carries none to re-send); the returned one is what to keep for next time.
|
|
148
|
+
*/
|
|
149
|
+
async function advanceWithCaptcha(client, state, values, opts) {
|
|
150
|
+
const { captcha, t } = opts;
|
|
151
|
+
const kind = state.attempt ? (0, attempt_1.nextStep)(state.attempt).kind : 'collect_identifier';
|
|
152
|
+
let identifier = opts.identifier ?? '';
|
|
153
|
+
let input = values;
|
|
154
|
+
let sentToken = false;
|
|
155
|
+
if (kind === 'collect_identifier' || kind === 'collect_captcha') {
|
|
156
|
+
if (values.identifier)
|
|
157
|
+
identifier = values.identifier;
|
|
158
|
+
if (captcha?.needed) {
|
|
159
|
+
const token = await captcha.getToken();
|
|
160
|
+
if (token) {
|
|
161
|
+
input = {
|
|
162
|
+
...values,
|
|
163
|
+
identifier: identifier || (state.attempt?.identifier ?? ''),
|
|
164
|
+
captcha_token: token,
|
|
165
|
+
};
|
|
166
|
+
sentToken = true;
|
|
167
|
+
}
|
|
168
|
+
else if (kind === 'collect_captcha') {
|
|
169
|
+
// Nothing to send until the widget yields a token.
|
|
170
|
+
return {
|
|
171
|
+
state: {
|
|
172
|
+
...state,
|
|
173
|
+
busy: false,
|
|
174
|
+
errors: [{ code: 'CAPTCHA_REQUIRED', message: t('captcha.required') }],
|
|
175
|
+
},
|
|
176
|
+
identifier,
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
let next = await (0, fapi_1.advance)(client, state, input);
|
|
182
|
+
const rechallenged = !!next.attempt && (0, attempt_1.nextStep)(next.attempt).kind === 'collect_captcha';
|
|
183
|
+
if (sentToken && rechallenged) {
|
|
184
|
+
next = { ...next, errors: [{ code: 'CAPTCHA_FAILED', message: t('captcha.failed') }] };
|
|
185
|
+
}
|
|
186
|
+
next = { ...next, errors: localizeCaptchaErrors(next.errors, t).errors };
|
|
187
|
+
if (sentToken && (rechallenged || next.errors.length > 0))
|
|
188
|
+
captcha?.reset();
|
|
189
|
+
return { state: next, identifier };
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* Create the account from a prebuilt create-account form: takes the sign-up
|
|
193
|
+
* captcha's token (when one is needed), POSTs `/v1/client/sign_ups` via
|
|
194
|
+
* {@link startSignUp}, localizes captcha refusals and, when the create was
|
|
195
|
+
* refused, draws a fresh widget (the token was spent either way).
|
|
196
|
+
*/
|
|
197
|
+
async function createSignUp(client, values, opts) {
|
|
198
|
+
const token = opts.captcha ? await opts.captcha.getToken() : null;
|
|
199
|
+
let next = await (0, fapi_1.startSignUp)(client, (0, instance_settings_1.toSignUpInput)(values, opts.consent, token));
|
|
200
|
+
next = { ...next, errors: localizeCaptchaErrors(next.errors, opts.t).errors };
|
|
201
|
+
if (next.errors.length > 0 && opts.captcha?.needed)
|
|
202
|
+
opts.captcha.reset();
|
|
203
|
+
return next;
|
|
204
|
+
}
|
|
205
|
+
//# sourceMappingURL=captcha-controller.js.map
|