@atlasauth/js 0.7.0 → 0.9.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 CHANGED
@@ -1,5 +1,52 @@
1
1
  # @atlasauth/js changelog
2
2
 
3
+ ## 0.9.0
4
+ - **Added `reason?: string` and `meta?: Record<string, unknown>` to `FieldError`**, parsed from the Frontend API
5
+ error envelope (`{ code, message, reason?, param?, meta? }`). `code` is unchanged. A network failure now carries
6
+ the client-side reason `network`.
7
+ - **Added `reason?: string` and `meta?: Record<string, unknown>` to `ApiKeyRequestError`** (new optional
8
+ constructor arguments `reason`, `meta` after `param`). `reauthenticate()` errors carry `.reason` too.
9
+ - **New: `localizedErrorMessage(error, strings?, fallback?)`** → the text to show for a server error: the catalog
10
+ string `error.<reason>` (placeholders filled from `meta`), else the server's `message`, else `error.generic`,
11
+ else an English generic sentence — never blank. `strings` is a flat catalog or a `(key) => string | undefined`
12
+ lookup (a lookup that echoes the key counts as missing). `ErrorStrings` accepts any object, so an
13
+ `interface`-typed bundle such as `esES` from `@atlasauth/localizations` passes without a cast.
14
+ - **New: `ERROR_REASONS`** (the 38 reasons the shipped catalogs translate), type **`ErrorReason`**,
15
+ **`errorReasonKey(reason)`** (`error.<reason>`), types **`ErrorLike`** and **`ErrorStrings`**, and
16
+ **`catalogLocale(catalog)`** (a bundle's own `locale` tag).
17
+ - **New: `FapiClientOptions.locale?: string`** — sent as `Accept-Language` on every request so the server's
18
+ `message` comes back in that language; `FapiClient.locale` getter and **`acceptLanguageFor(tag)`** (normalises
19
+ `pt_BR` → `pt-BR`, rejects anything that is not a plain BCP-47 tag). **`ReauthOptions.locale?: string`** does the
20
+ same for `reauthenticate()`.
21
+
22
+
23
+ ## 0.8.0
24
+ - **New: `SignOutGuard`** — the safer sign-out every framework provider now drives, so they all behave like
25
+ `@atlasauth/react`: `new SignOutGuard({ request, frontendApi?, store?, tombstones?, retryDelaysMs?,
26
+ frontChannelLogout? })` (`SignOutGuardOptions`). Methods: `revoke(sessionId, { frontchannel?, request? })` →
27
+ `SignOutAttempt` (`{ sessionId, outcome }`; finds the cookie session with `GET /v1/client` when the id is unknown,
28
+ tombstones it before revoking, revokes with one retry, runs front-channel logout, and re-tries a failed revoke in
29
+ the background after 15 s, 1 min and 5 min); `settle(attempt, { strict? })` → `SignOutResult` (throws
30
+ `SignOutError` under `strict`); `promoted(attempt)` → the server-promoted session id to adopt, or null;
31
+ `refuse(sessionId)` → whether a booted session must not be adopted (re-tries its revoke in the background and
32
+ removes the tombstone once it lands); `isSignedOut(id)`; `signal(attempt)` / `learn(message)` for the cross-tab
33
+ `{ signedOut: string[] }` message; `watch(current, onSignedOut)` (another tab signed out of this session);
34
+ `ready`; `dispose()`; `tombstones`. **Added types** `FapiRequest`, `SignOutGuardOptions`, `SignOutAttempt`,
35
+ `SignedOutSignal`.
36
+ - **Tombstones on platforms without `localStorage`.** `SessionTombstonesOptions.store?: TombstoneStore` (new type:
37
+ `{ getItem(key), setItem(key, value) }`, sync or async) keeps the tombstones in the platform's own store
38
+ (AsyncStorage, `expo-secure-store`, `chrome.storage`, Capacitor Preferences…). **Added**
39
+ `SessionTombstones.ready: Promise<void>` (settles once the store has been read; immediate for `localStorage`).
40
+ - **Email delivery.** `InstanceSettings.emailDelivery: EmailDelivery` (new type: `{ available, passwordReset,
41
+ passwordSignUp }`, read from `email_delivery` on `GET /v1/appearance`; all `true` on a server that does not send
42
+ it). **New helpers:** `emailDeliveryFrom(body)` (an environment / appearance body or the object itself),
43
+ `offeredFirstFactors(strategies, delivery)` (drops `email_code` / `email_link` while email can't be delivered),
44
+ and the constants `EMAIL_DELIVERY_UNKNOWN`, `EMAIL_UNAVAILABLE_MESSAGE`, `EMAIL_FIRST_FACTORS`.
45
+ - **New: typed client environment.** `fetchClientEnvironment(client)` → `ClientEnvironment | null` and
46
+ `clientEnvironmentFrom(body)` for `GET /v1/client/environment`: `{ firstFactors, strategies, emailDelivery,
47
+ socialProviders }`, with the new `EnvironmentSocialProvider` type (`provider`, `native`, `mode`, `label`,
48
+ `display_name`, `icon`, `allow_sign_in`, `allow_sign_up`, `client_id?`, `bot_username?`).
49
+
3
50
  ## 0.7.0
4
51
  - **Popup OAuth completes when the opener page sends `Cross-Origin-Opener-Policy: same-origin`.** That
5
52
  header puts the provider popup in another browsing-context group: `popup.closed` reads `true` at once and
@@ -62,7 +62,15 @@ export declare class ApiKeyRequestError extends Error {
62
62
  readonly code: string;
63
63
  readonly status: number;
64
64
  readonly param?: string;
65
- constructor(status: number, code: string, message: string, param?: string);
65
+ /**
66
+ * The server's stable reason (`too_many_attempts`, …), when it sent one: the
67
+ * catalog key `error.<reason>` a UI shows instead of `message`. See
68
+ * `localizedErrorMessage`.
69
+ */
70
+ readonly reason?: string;
71
+ /** The server's `meta` detail for this error, when it sent one. */
72
+ readonly meta?: Record<string, unknown>;
73
+ constructor(status: number, code: string, message: string, param?: string, reason?: string, meta?: Record<string, unknown>);
66
74
  /** True when the action needs a fresh re-authentication first. */
67
75
  get needsReverification(): boolean;
68
76
  }
package/dist/api-keys.js CHANGED
@@ -10,12 +10,24 @@ class ApiKeyRequestError extends Error {
10
10
  code;
11
11
  status;
12
12
  param;
13
- constructor(status, code, message, param) {
13
+ /**
14
+ * The server's stable reason (`too_many_attempts`, …), when it sent one: the
15
+ * catalog key `error.<reason>` a UI shows instead of `message`. See
16
+ * `localizedErrorMessage`.
17
+ */
18
+ reason;
19
+ /** The server's `meta` detail for this error, when it sent one. */
20
+ meta;
21
+ constructor(status, code, message, param, reason, meta) {
14
22
  super(message);
15
23
  this.name = 'ApiKeyRequestError';
16
24
  this.status = status;
17
25
  this.code = code;
18
26
  this.param = param;
27
+ if (reason)
28
+ this.reason = reason;
29
+ if (meta)
30
+ this.meta = meta;
19
31
  }
20
32
  /** True when the action needs a fresh re-authentication first. */
21
33
  get needsReverification() {
@@ -28,7 +40,7 @@ async function call(client, path, init = {}) {
28
40
  const res = await client.request(path, init);
29
41
  if (!res.ok || !res.data) {
30
42
  const first = res.errors[0];
31
- throw new ApiKeyRequestError(res.status, first?.code ?? 'UNKNOWN', first?.message ?? 'The request failed.', first?.param);
43
+ throw new ApiKeyRequestError(res.status, first?.code ?? 'UNKNOWN', first?.message ?? 'The request failed.', first?.param, first?.reason, first?.meta);
32
44
  }
33
45
  return res.data;
34
46
  }
@@ -1 +1 @@
1
- {"version":3,"file":"api-keys.js","sourceRoot":"","sources":["../src/api-keys.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"api-keys.js","sourceRoot":"","sources":["../src/api-keys.ts"],"names":[],"mappings":"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"}
package/dist/attempt.d.ts CHANGED
@@ -78,6 +78,19 @@ export interface FieldError {
78
78
  code: string;
79
79
  message: string;
80
80
  param?: string;
81
+ /**
82
+ * The server's stable, machine-readable reason for this error
83
+ * (`form_password_incorrect`, `too_many_attempts`, …), finer-grained than
84
+ * `code`. Prebuilt UIs look it up as the catalog key `error.<reason>` to show
85
+ * the message in the app's language; see `localizedErrorMessage`. Absent on
86
+ * servers that predate it and on errors raised client-side.
87
+ */
88
+ reason?: string;
89
+ /**
90
+ * Extra machine-readable detail the server attached (`min_length`,
91
+ * `providers`, …); also the interpolation values for `error.<reason>`.
92
+ */
93
+ meta?: Record<string, unknown>;
81
94
  }
82
95
  export declare function fieldErrors(body: unknown): FieldError[];
83
96
  /** Errors for one field, so a form can render them inline. */
package/dist/attempt.js CHANGED
@@ -76,7 +76,16 @@ function fieldErrors(body) {
76
76
  .filter((error) => {
77
77
  return Boolean(error) && typeof error.message === 'string';
78
78
  })
79
- .map((error) => ({ code: error.code, message: error.message, param: error.param }));
79
+ .map((error) => {
80
+ const out = { code: error.code, message: error.message, param: error.param };
81
+ const reason = error.reason;
82
+ if (typeof reason === 'string' && reason !== '')
83
+ out.reason = reason;
84
+ const meta = error.meta;
85
+ if (meta && typeof meta === 'object' && !Array.isArray(meta))
86
+ out.meta = meta;
87
+ return out;
88
+ });
80
89
  }
81
90
  /** Errors for one field, so a form can render them inline. */
82
91
  function errorsFor(errors, param) {
@@ -1 +1 @@
1
- {"version":3,"file":"attempt.js","sourceRoot":"","sources":["../src/attempt.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;GAYG;;AAkDH,4BAqDC;AAGD,gCAEC;AAgBD,kCAUC;AAGD,8BAEC;AAGD,gCAEC;AA9FD,SAAgB,QAAQ,CAAC,OAAoB;IAC3C,QAAQ,OAAO,CAAC,MAAuB,EAAE,CAAC;QACxC,KAAK,kBAAkB;YACrB,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC;QAExC,KAAK,oBAAoB;YACvB,OAAO;gBACL,IAAI,EAAE,sBAAsB;gBAC5B,sEAAsE;gBACtE,qEAAqE;gBACrE,uEAAuE;gBACvE,wCAAwC;gBACxC,UAAU,EAAE,OAAO,CAAC,uBAAuB,IAAI,EAAE;aAClD,CAAC;QAEJ,KAAK,qBAAqB;YACxB,OAAO,EAAE,IAAI,EAAE,uBAAuB,EAAE,CAAC;QAE3C;;;;;WAKG;QACH,KAAK,sBAAsB;YACzB,OAAO,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAC;QAE1C,KAAK,0BAA0B;YAC7B,OAAO,EAAE,IAAI,EAAE,oBAAoB,EAAE,CAAC;QAExC,KAAK,eAAe;YAClB,OAAO,EAAE,IAAI,EAAE,iBAAiB,EAAE,CAAC;QAErC,KAAK,sBAAsB;YACzB,OAAO,EAAE,IAAI,EAAE,aAAa,EAAE,CAAC;QAEjC,KAAK,oBAAoB;YACvB,OAAO,EAAE,IAAI,EAAE,sBAAsB,EAAE,CAAC;QAE1C,KAAK,UAAU;YACb,OAAO,EAAE,IAAI,EAAE,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,kBAAkB,IAAI,IAAI,EAAE,CAAC;QAEzE,KAAK,WAAW;YACd,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,WAAW,EAAE,CAAC;QAElD;YACE;;;;eAIG;YACH,OAAO,EAAE,IAAI,EAAE,SAAS,EAAE,MAAM,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC;IACvD,CAAC;AACH,CAAC;AAED,iFAAiF;AACjF,SAAgB,UAAU,CAAC,OAAoB;IAC7C,OAAO,OAAO,CAAC,MAAM,KAAK,UAAU,IAAI,OAAO,CAAC,MAAM,KAAK,WAAW,CAAC;AACzE,CAAC;AAgBD,SAAgB,WAAW,CAAC,IAAa;IACvC,IAAI,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ;QAAE,OAAO,EAAE,CAAC;IACjD,MAAM,MAAM,GAAI,IAA6B,CAAC,MAAM,CAAC;IACrD,IAAI,CAAC,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC;QAAE,OAAO,EAAE,CAAC;IAEtC,OAAO,MAAM;SACV,MAAM,CAAC,CAAC,KAAK,EAAuB,EAAE;QACrC,OAAO,OAAO,CAAC,KAAK,CAAC,IAAI,OAAQ,KAAoB,CAAC,OAAO,KAAK,QAAQ,CAAC;IAC7E,CAAC,CAAC;SACD,GAAG,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,EAAE,KAAK,CAAC,IAAI,EAAE,OAAO,EAAE,KAAK,CAAC,OAAO,EAAE,KAAK,EAAE,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;AACxF,CAAC;AAED,8DAA8D;AAC9D,SAAgB,SAAS,CAAC,MAA6B,EAAE,KAAa;IACpE,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC;AACzD,CAAC;AAED,iEAAiE;AACjE,SAAgB,UAAU,CAAC,MAA6B;IACtD,OAAO,MAAM,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;AAChD,CAAC"}
1
+ {"version":3,"file":"attempt.js","sourceRoot":"","sources":["../src/attempt.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,79 @@
1
+ import type { FapiClient } from './fapi';
2
+ /**
3
+ * The client environment (`GET /v1/client/environment`) and the email-delivery
4
+ * facts both it and `GET /v1/appearance` publish.
5
+ *
6
+ * An app that cannot deliver email (a production instance with no email
7
+ * provider) cannot send a reset code, an email sign-in code or link, or a
8
+ * sign-up verification code. The server already stops offering email code / link
9
+ * and refuses those requests; `email_delivery` lets a sign-in UI say so up front
10
+ * instead: hide "Forgot password?" and email-code options, and show a "can't
11
+ * send email" state where a sign-up would need one.
12
+ */
13
+ /** `email_delivery`: what works given whether the app can deliver email. */
14
+ export interface EmailDelivery {
15
+ /** The app can send email (its own provider, or the shared development sender). */
16
+ available: boolean;
17
+ /** "Forgot password?" can work: password sign-in is on and reset emails can be delivered. */
18
+ passwordReset: boolean;
19
+ /** Email + password sign-up can complete (email can be delivered, or verification is not required). */
20
+ passwordSignUp: boolean;
21
+ }
22
+ /**
23
+ * What an older server (no `email_delivery`) is taken to mean: everything works.
24
+ * The server still refuses whatever it cannot deliver.
25
+ */
26
+ export declare const EMAIL_DELIVERY_UNKNOWN: Readonly<EmailDelivery>;
27
+ /** The default English copy for the "can't send email" state. */
28
+ export declare const EMAIL_UNAVAILABLE_MESSAGE = "This app can't send email right now. Try another sign-in method or contact the app owner.";
29
+ /** The first factors that need the app to deliver email. */
30
+ export declare const EMAIL_FIRST_FACTORS: readonly string[];
31
+ /**
32
+ * Read `email_delivery` from a `/v1/client/environment` or `/v1/appearance`
33
+ * body — or from the `email_delivery` object itself. A missing object or field
34
+ * reads as available (see {@link EMAIL_DELIVERY_UNKNOWN}).
35
+ */
36
+ export declare function emailDeliveryFrom(body: unknown): EmailDelivery;
37
+ /**
38
+ * The first factors a sign-in UI should offer: `strategies` without email code /
39
+ * link while the app cannot deliver email. A capability of the app, not of the
40
+ * user, so it never narrows the server's uniform list per identifier. With no
41
+ * `delivery` (not loaded yet) the list is returned unchanged.
42
+ */
43
+ export declare function offeredFirstFactors(strategies: readonly string[], delivery: EmailDelivery | null | undefined): string[];
44
+ /** One entry of the environment's `social_providers`. */
45
+ export interface EnvironmentSocialProvider {
46
+ provider: string;
47
+ /** Whether the provider signs in natively (One Tap etc.) on this instance. */
48
+ native: boolean;
49
+ mode: 'native' | 'redirect';
50
+ /** The full button text, or null for the SDK's own label. */
51
+ label: string | null;
52
+ display_name: string | null;
53
+ icon: {
54
+ type: 'url' | 'svg';
55
+ value: string;
56
+ } | null;
57
+ allow_sign_in: boolean;
58
+ allow_sign_up: boolean;
59
+ /** Public client id, for native providers only. */
60
+ client_id?: string;
61
+ /** Telegram's bot username. */
62
+ bot_username?: string;
63
+ }
64
+ /** `GET /v1/client/environment`, typed. */
65
+ export interface ClientEnvironment {
66
+ /** The enabled first factors (`password`, `email_code`, `passkey`, …). */
67
+ firstFactors: string[];
68
+ /**
69
+ * The offered strategies, camelCase as the server sends them (`password`,
70
+ * `emailCode`, `emailLink`, `passkey`, `phoneCode`, …, `oauthProviders`).
71
+ */
72
+ strategies: Record<string, unknown>;
73
+ emailDelivery: EmailDelivery;
74
+ socialProviders: EnvironmentSocialProvider[];
75
+ }
76
+ /** Read a `/v1/client/environment` body. Defensive: malformed parts read as empty. */
77
+ export declare function clientEnvironmentFrom(body: unknown): ClientEnvironment;
78
+ /** Fetch the client environment. Null when the server can't be reached or errors. */
79
+ export declare function fetchClientEnvironment(client: FapiClient): Promise<ClientEnvironment | null>;
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.EMAIL_FIRST_FACTORS = exports.EMAIL_UNAVAILABLE_MESSAGE = exports.EMAIL_DELIVERY_UNKNOWN = void 0;
4
+ exports.emailDeliveryFrom = emailDeliveryFrom;
5
+ exports.offeredFirstFactors = offeredFirstFactors;
6
+ exports.clientEnvironmentFrom = clientEnvironmentFrom;
7
+ exports.fetchClientEnvironment = fetchClientEnvironment;
8
+ /**
9
+ * What an older server (no `email_delivery`) is taken to mean: everything works.
10
+ * The server still refuses whatever it cannot deliver.
11
+ */
12
+ exports.EMAIL_DELIVERY_UNKNOWN = Object.freeze({
13
+ available: true,
14
+ passwordReset: true,
15
+ passwordSignUp: true,
16
+ });
17
+ /** The default English copy for the "can't send email" state. */
18
+ exports.EMAIL_UNAVAILABLE_MESSAGE = "This app can't send email right now. Try another sign-in method or contact the app owner.";
19
+ /** The first factors that need the app to deliver email. */
20
+ exports.EMAIL_FIRST_FACTORS = ['email_code', 'email_link'];
21
+ const isRecord = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
22
+ /**
23
+ * Read `email_delivery` from a `/v1/client/environment` or `/v1/appearance`
24
+ * body — or from the `email_delivery` object itself. A missing object or field
25
+ * reads as available (see {@link EMAIL_DELIVERY_UNKNOWN}).
26
+ */
27
+ function emailDeliveryFrom(body) {
28
+ const raw = isRecord(body) && isRecord(body.email_delivery) ? body.email_delivery : body;
29
+ if (!isRecord(raw) || !('available' in raw || 'password_reset' in raw || 'password_sign_up' in raw)) {
30
+ return { ...exports.EMAIL_DELIVERY_UNKNOWN };
31
+ }
32
+ const flag = (v) => (typeof v === 'boolean' ? v : true);
33
+ return {
34
+ available: flag(raw.available),
35
+ passwordReset: flag(raw.password_reset),
36
+ passwordSignUp: flag(raw.password_sign_up),
37
+ };
38
+ }
39
+ /**
40
+ * The first factors a sign-in UI should offer: `strategies` without email code /
41
+ * link while the app cannot deliver email. A capability of the app, not of the
42
+ * user, so it never narrows the server's uniform list per identifier. With no
43
+ * `delivery` (not loaded yet) the list is returned unchanged.
44
+ */
45
+ function offeredFirstFactors(strategies, delivery) {
46
+ if (!delivery || delivery.available)
47
+ return [...strategies];
48
+ return strategies.filter((s) => !exports.EMAIL_FIRST_FACTORS.includes(s));
49
+ }
50
+ /** Read a `/v1/client/environment` body. Defensive: malformed parts read as empty. */
51
+ function clientEnvironmentFrom(body) {
52
+ const root = isRecord(body) ? body : {};
53
+ return {
54
+ firstFactors: Array.isArray(root.first_factors)
55
+ ? root.first_factors.filter((f) => typeof f === 'string')
56
+ : [],
57
+ strategies: isRecord(root.strategies) ? { ...root.strategies } : {},
58
+ emailDelivery: emailDeliveryFrom(root),
59
+ socialProviders: Array.isArray(root.social_providers)
60
+ ? root.social_providers.filter((p) => isRecord(p) && typeof p.provider === 'string')
61
+ : [],
62
+ };
63
+ }
64
+ /** Fetch the client environment. Null when the server can't be reached or errors. */
65
+ async function fetchClientEnvironment(client) {
66
+ try {
67
+ const response = await client.get('/v1/client/environment');
68
+ return response.ok ? clientEnvironmentFrom(response.data) : null;
69
+ }
70
+ catch {
71
+ return null;
72
+ }
73
+ }
74
+ //# sourceMappingURL=environment.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"environment.js","sourceRoot":"","sources":["../src/environment.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Server errors in the app's language.
3
+ *
4
+ * A Frontend API error carries a human `message` (in English unless the request
5
+ * sent an `Accept-Language` the server has strings for) and, on current
6
+ * servers, a stable `reason` such as `form_password_incorrect`. A prebuilt form
7
+ * looks the reason up as the catalog key `error.<reason>`, so a Spanish form
8
+ * shows a Spanish sentence even when the server answered in English, and an
9
+ * app can reword any error by overriding that one key.
10
+ */
11
+ /**
12
+ * The reasons the Frontend API sends that every shipped catalog translates, as
13
+ * `error.<reason>` keys. Listed for docs and coverage checks; a reason missing
14
+ * from this list still renders (as the server's `message`).
15
+ */
16
+ export declare const ERROR_REASONS: readonly ["form_password_incorrect", "form_code_incorrect", "form_code_already_used", "form_code_expired", "form_code_max_attempts", "form_link_invalid", "form_identifier_not_found", "form_identifier_exists", "form_password_pwned", "form_password_too_short", "form_password_too_long", "form_password_requires_uppercase", "form_password_requires_lowercase", "form_password_requires_number", "form_password_requires_symbol", "form_password_not_allowed", "form_password_contains_identifier", "form_password_same_as_current", "form_password_reused", "password_expired", "email_not_verified", "sign_in_token_invalid", "too_many_attempts", "too_many_requests", "captcha_required", "captcha_failed", "captcha_unavailable", "email_delivery_unavailable", "strategy_disabled", "sign_up_restricted", "waitlisted", "identifier_not_allowed", "disposable_email_blocked", "sso_required", "sign_ins_disabled", "sign_ups_disabled", "high_risk_blocked", "sms_region_blocked"];
17
+ /** A reason with a string in every shipped catalog. */
18
+ export type ErrorReason = (typeof ERROR_REASONS)[number];
19
+ /** The catalog key for a reason: `error.<reason>`. */
20
+ export declare function errorReasonKey(reason: string): string;
21
+ /**
22
+ * What {@link localizedErrorMessage} reads strings from: a flat catalog object
23
+ * (`{ 'error.too_many_attempts': '…' }`, or a bundle such as `esES` from
24
+ * `@atlasauth/localizations`) or a lookup function. A function may answer a
25
+ * missing key with `undefined`, `null`, `''` or the key itself (the convention
26
+ * of the prebuilt UIs' `translate`); all four count as "no string". Typed as
27
+ * `object` so an `interface`-typed bundle is accepted without a cast.
28
+ */
29
+ export type ErrorStrings = ((key: string) => string | null | undefined) | object;
30
+ /** Anything error-shaped: a `FieldError`, an `ApiKeyRequestError`, an `Error`, a server error object. */
31
+ export interface ErrorLike {
32
+ reason?: string | null;
33
+ message?: string | null;
34
+ code?: string | null;
35
+ /** Interpolation values for the catalog string (`{min_length}` ← `meta.min_length`). */
36
+ meta?: Readonly<Record<string, unknown>> | null;
37
+ }
38
+ /**
39
+ * The message to show for a server error, in the app's language:
40
+ *
41
+ * 1. the catalog string `error.<reason>`, when the error has a reason and
42
+ * `strings` has that key, with `{placeholders}` filled from the error's
43
+ * `meta` (a placeholder `meta` cannot fill skips to step 2);
44
+ * 2. else the server's `message` (localised by the server when the client sent
45
+ * `Accept-Language` — see `FapiClientOptions.locale`);
46
+ * 3. else the catalog's `error.generic`, else an English generic sentence.
47
+ *
48
+ * Never returns an empty string. `fallback`, when given, is consulted for
49
+ * `error.generic` after `strings` — handy when `strings` is a language layer
50
+ * without the English defaults underneath it.
51
+ */
52
+ export declare function localizedErrorMessage(error: ErrorLike | string | null | undefined, strings?: ErrorStrings | null, fallback?: ErrorStrings | null): string;
53
+ /**
54
+ * The BCP-47 tag a catalog declares for itself (`@atlasauth/localizations`
55
+ * bundles carry `locale: 'es-ES'`), or undefined. Prebuilt UIs that take a
56
+ * `localization` bundle use it as the `Accept-Language` they send.
57
+ */
58
+ export declare function catalogLocale(catalog: object | null | undefined): string | undefined;
package/dist/errors.js ADDED
@@ -0,0 +1,143 @@
1
+ "use strict";
2
+ /**
3
+ * Server errors in the app's language.
4
+ *
5
+ * A Frontend API error carries a human `message` (in English unless the request
6
+ * sent an `Accept-Language` the server has strings for) and, on current
7
+ * servers, a stable `reason` such as `form_password_incorrect`. A prebuilt form
8
+ * looks the reason up as the catalog key `error.<reason>`, so a Spanish form
9
+ * shows a Spanish sentence even when the server answered in English, and an
10
+ * app can reword any error by overriding that one key.
11
+ */
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.ERROR_REASONS = void 0;
14
+ exports.errorReasonKey = errorReasonKey;
15
+ exports.localizedErrorMessage = localizedErrorMessage;
16
+ exports.catalogLocale = catalogLocale;
17
+ /**
18
+ * The reasons the Frontend API sends that every shipped catalog translates, as
19
+ * `error.<reason>` keys. Listed for docs and coverage checks; a reason missing
20
+ * from this list still renders (as the server's `message`).
21
+ */
22
+ exports.ERROR_REASONS = Object.freeze([
23
+ 'form_password_incorrect',
24
+ 'form_code_incorrect',
25
+ 'form_code_already_used',
26
+ 'form_code_expired',
27
+ 'form_code_max_attempts',
28
+ 'form_link_invalid',
29
+ 'form_identifier_not_found',
30
+ 'form_identifier_exists',
31
+ 'form_password_pwned',
32
+ 'form_password_too_short',
33
+ 'form_password_too_long',
34
+ 'form_password_requires_uppercase',
35
+ 'form_password_requires_lowercase',
36
+ 'form_password_requires_number',
37
+ 'form_password_requires_symbol',
38
+ 'form_password_not_allowed',
39
+ 'form_password_contains_identifier',
40
+ 'form_password_same_as_current',
41
+ 'form_password_reused',
42
+ 'password_expired',
43
+ 'email_not_verified',
44
+ 'sign_in_token_invalid',
45
+ 'too_many_attempts',
46
+ 'too_many_requests',
47
+ 'captcha_required',
48
+ 'captcha_failed',
49
+ 'captcha_unavailable',
50
+ 'email_delivery_unavailable',
51
+ 'strategy_disabled',
52
+ 'sign_up_restricted',
53
+ 'waitlisted',
54
+ 'identifier_not_allowed',
55
+ 'disposable_email_blocked',
56
+ 'sso_required',
57
+ 'sign_ins_disabled',
58
+ 'sign_ups_disabled',
59
+ 'high_risk_blocked',
60
+ 'sms_region_blocked',
61
+ ]);
62
+ /** The catalog key for a reason: `error.<reason>`. */
63
+ function errorReasonKey(reason) {
64
+ return `error.${reason}`;
65
+ }
66
+ /** The English fallback of last resort, so the result is never blank. */
67
+ const GENERIC_ERROR = 'Something went wrong. Please try again.';
68
+ /** A reason is a lower-case token; anything else is never used as a key. */
69
+ const REASON = /^[a-z][a-z0-9_]{0,63}$/;
70
+ /**
71
+ * Fill `{name}` from `meta` (string or finite-number values). Null when a
72
+ * placeholder has no usable value: a sentence with a hole in it is worse than
73
+ * the server's own message, which the server has already filled.
74
+ */
75
+ function interpolate(template, meta) {
76
+ let complete = true;
77
+ const text = template.replace(/\{(\w+)\}/g, (match, name) => {
78
+ const value = meta && Object.prototype.hasOwnProperty.call(meta, name) ? meta[name] : undefined;
79
+ if (typeof value === 'string' && value !== '')
80
+ return value;
81
+ if (typeof value === 'number' && Number.isFinite(value))
82
+ return String(value);
83
+ complete = false;
84
+ return match;
85
+ });
86
+ return complete ? text : null;
87
+ }
88
+ function lookup(strings, key) {
89
+ if (!strings)
90
+ return null;
91
+ let value;
92
+ if (typeof strings === 'function') {
93
+ value = strings(key);
94
+ }
95
+ else {
96
+ value = Object.prototype.hasOwnProperty.call(strings, key)
97
+ ? strings[key]
98
+ : undefined;
99
+ }
100
+ if (typeof value !== 'string')
101
+ return null;
102
+ const text = value.trim();
103
+ return text === '' || text === key ? null : value;
104
+ }
105
+ /**
106
+ * The message to show for a server error, in the app's language:
107
+ *
108
+ * 1. the catalog string `error.<reason>`, when the error has a reason and
109
+ * `strings` has that key, with `{placeholders}` filled from the error's
110
+ * `meta` (a placeholder `meta` cannot fill skips to step 2);
111
+ * 2. else the server's `message` (localised by the server when the client sent
112
+ * `Accept-Language` — see `FapiClientOptions.locale`);
113
+ * 3. else the catalog's `error.generic`, else an English generic sentence.
114
+ *
115
+ * Never returns an empty string. `fallback`, when given, is consulted for
116
+ * `error.generic` after `strings` — handy when `strings` is a language layer
117
+ * without the English defaults underneath it.
118
+ */
119
+ function localizedErrorMessage(error, strings, fallback) {
120
+ const err = typeof error === 'string' ? { message: error } : (error ?? {});
121
+ const reason = typeof err.reason === 'string' ? err.reason : '';
122
+ if (reason && REASON.test(reason)) {
123
+ const template = lookup(strings, errorReasonKey(reason));
124
+ const text = template ? interpolate(template, err.meta) : null;
125
+ if (text)
126
+ return text;
127
+ }
128
+ if (typeof err.message === 'string' && err.message.trim() !== '')
129
+ return err.message;
130
+ return lookup(strings, 'error.generic') ?? lookup(fallback, 'error.generic') ?? GENERIC_ERROR;
131
+ }
132
+ /**
133
+ * The BCP-47 tag a catalog declares for itself (`@atlasauth/localizations`
134
+ * bundles carry `locale: 'es-ES'`), or undefined. Prebuilt UIs that take a
135
+ * `localization` bundle use it as the `Accept-Language` they send.
136
+ */
137
+ function catalogLocale(catalog) {
138
+ if (!catalog || typeof catalog !== 'object')
139
+ return undefined;
140
+ const tag = catalog.locale;
141
+ return typeof tag === 'string' && tag.trim() !== '' ? tag : undefined;
142
+ }
143
+ //# sourceMappingURL=errors.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.js","sourceRoot":"","sources":["../src/errors.ts"],"names":[],"mappings":"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"}
package/dist/fapi.d.ts CHANGED
@@ -18,7 +18,22 @@ export interface FapiClientOptions {
18
18
  /** The instance's FAPI origin. Empty for same-origin. */
19
19
  baseUrl?: string;
20
20
  fetchImpl?: typeof fetch;
21
+ /**
22
+ * The app's UI language as a BCP-47 tag (`"es"`, `"pt-BR"`). When set, every
23
+ * request carries it as `Accept-Language`, so the server writes its error
24
+ * `message`s in that language — the fallback a prebuilt form shows for an
25
+ * error whose `reason` its catalog has no string for. Unset sends no header
26
+ * (the browser's own `Accept-Language` applies). A malformed tag is ignored.
27
+ */
28
+ locale?: string;
21
29
  }
30
+ /**
31
+ * A locale tag normalised for the `Accept-Language` header (`pt_BR` → `pt-BR`),
32
+ * or null when it is not a plausible BCP-47 tag. Restricting it to letters,
33
+ * digits and hyphens keeps the header CORS-safelisted, so sending it never adds
34
+ * a preflight to a cross-origin Frontend API call.
35
+ */
36
+ export declare function acceptLanguageFor(locale: string | null | undefined): string | null;
22
37
  export interface FapiResponse<T> {
23
38
  ok: boolean;
24
39
  status: number;
@@ -28,6 +43,8 @@ export interface FapiResponse<T> {
28
43
  export declare class FapiClient {
29
44
  private readonly options;
30
45
  constructor(options: FapiClientOptions);
46
+ /** The `Accept-Language` this client sends, or null when it sends none. */
47
+ get locale(): string | null;
31
48
  /**
32
49
  * The origin FAPI serves from — where an OAuth popup's callback page lives, so
33
50
  * the popup flow can pin `postMessage`'s `event.origin` to it. Empty `baseUrl`
package/dist/fapi.js CHANGED
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.initialFlow = exports.FapiClient = void 0;
4
+ exports.acceptLanguageFor = acceptLanguageFor;
4
5
  exports.getProviderToken = getProviderToken;
5
6
  exports.requestForStep = requestForStep;
6
7
  exports.flowFromPending = flowFromPending;
@@ -10,11 +11,27 @@ exports.prepareFactor = prepareFactor;
10
11
  exports.pollAttempt = pollAttempt;
11
12
  exports.shouldKeepPolling = shouldKeepPolling;
12
13
  const attempt_1 = require("./attempt");
14
+ /**
15
+ * A locale tag normalised for the `Accept-Language` header (`pt_BR` → `pt-BR`),
16
+ * or null when it is not a plausible BCP-47 tag. Restricting it to letters,
17
+ * digits and hyphens keeps the header CORS-safelisted, so sending it never adds
18
+ * a preflight to a cross-origin Frontend API call.
19
+ */
20
+ function acceptLanguageFor(locale) {
21
+ if (typeof locale !== 'string')
22
+ return null;
23
+ const tag = locale.trim().replace(/_/g, '-');
24
+ return /^[A-Za-z]{2,8}(-[A-Za-z0-9]{1,8})*$/.test(tag) ? tag : null;
25
+ }
13
26
  class FapiClient {
14
27
  options;
15
28
  constructor(options) {
16
29
  this.options = options;
17
30
  }
31
+ /** The `Accept-Language` this client sends, or null when it sends none. */
32
+ get locale() {
33
+ return acceptLanguageFor(this.options.locale);
34
+ }
18
35
  /**
19
36
  * The origin FAPI serves from — where an OAuth popup's callback page lives, so
20
37
  * the popup flow can pin `postMessage`'s `event.origin` to it. Empty `baseUrl`
@@ -33,6 +50,7 @@ class FapiClient {
33
50
  }
34
51
  async request(path, init = {}) {
35
52
  const doFetch = this.options.fetchImpl ?? fetch;
53
+ const language = this.locale;
36
54
  let response;
37
55
  try {
38
56
  response = await doFetch(`${this.options.baseUrl ?? ''}${path}`, {
@@ -42,6 +60,7 @@ class FapiClient {
42
60
  credentials: 'include',
43
61
  headers: {
44
62
  'x-publishable-key': this.options.publishableKey,
63
+ ...(language ? { 'accept-language': language } : {}),
45
64
  ...(init.body ? { 'content-type': 'application/json' } : {}),
46
65
  ...init.headers,
47
66
  },
@@ -57,7 +76,7 @@ class FapiClient {
57
76
  ok: false,
58
77
  status: 0,
59
78
  data: null,
60
- errors: [{ code: 'NETWORK', message: 'We could not reach the server.' }],
79
+ errors: [{ code: 'NETWORK', reason: 'network', message: 'We could not reach the server.' }],
61
80
  };
62
81
  }
63
82
  let body = null;