pgkiln 0.60.0 → 0.62.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/SECURITY.md CHANGED
@@ -113,6 +113,8 @@ first version (MVP). Every finding below has a regression test in
113
113
  | Ask and smart filters set facets (sprint 122) | A phrase can only set facets of the faceted search and smart filters regions on the page that the user sees, on their configured columns; values and range bounds become the facets' own URL parameters (bounds checked as numbers or dates) and reach SQL as query parameters, never as text. Values in a phrase are looked up in the report's rows with a query parameter, as the application's role. A facet chip's Remove is checked against the chips the form showed (a tampered key is refused). Sample apps keep their demo accounts with known passwords: install them on development or demo instances only. |
114
114
  | Builder texts in the browser (sprint 123) | The builder's JavaScript gets its texts as JSON in an attribute (`<div id="builder-texts" data-texts>`), escaped like any attribute value, so builder pages still have no inline script. The texts are the fixed message files, never data; scripts put them into the page with `textContent` and attributes, not as HTML. |
115
115
  | Two-factor sign-in for developers (0.60.0) | TOTP secrets (RFC 6238, 160 bits) are encrypted with `PGKILN_SECRET_KEY` and stored only after a code from the app confirms them; recovery codes (10 × 50 bits) are stored as SHA-256 and used up atomically; a code is accepted once (the last step is updated atomically) and only one 30-second step either side. The password step leaves only a pending sign-in in the session (5 minutes, no session for the developer yet); wrong codes count with wrong passwords for the per-user and per-IP lockout. Removing an app needs the password; another developer's factors can only be reset by an administrator, with a reason, which ends that developer's builder sessions. The require policy can only be turned on, and stays enforceable, with at least two administrators (or every one) holding an app; there is no setting, URL or variable that skips the second step: recovery is `pgkiln admin reset-2fa` / `set-2fa-policy off` on the server with the owner connection. Every enrolment, removal, reset, policy change and used recovery code is in the activity log; resets and policy changes are mailed to the administrators. The factor tables are closed to the runtime role. |
116
+ | Passkeys and trusted browsers (0.61.0) | Passkeys (WebAuthn) are checked in `src/webauthn.ts` without a library: a 32-byte one-time challenge per purpose (registration or sign-in) kept in the session for five minutes and used up on the first answer, the `clientDataJSON` type, challenge and origin (`PUBLIC_URL`'s, else the browser's own), the SHA-256 of the relying party id in the authenticator data, user presence, an ES256, EdDSA or RS256 signature over the authenticator data and the client data hash, and a signature counter that must go up (updated atomically, so a replayed or cloned key is refused). The CBOR reader takes only definite lengths, at most 16 levels and 1,000 entries. Credentials are public keys (no secret to leak, independent of the instance key); attestation is *none*. `PGKILN_RP_ID` is only honoured when it is the host or a parent domain of it. The sign-in options (the developer's credential ids) are only given after the password step, with the CSRF token. Trusted browsers (off by default) are a random 256-bit cookie token (`HttpOnly`, `SameSite=Lax`, `Secure` with `COOKIE_SECURE`, path `/builder`) of which only the SHA-256 is stored; the cookie is only looked at after a correct password, for that developer, and ends with its days, a new password, removing a factor, a reset, *Forget trusted browsers* or turning trust off (which removes every trusted browser at once). |
117
+ | Passkey sign-in without a password, workspace rule (0.62.0) | *Sign in with a passkey* asks the browser for any passkey of this site (empty allow list) with `userVerification: required`, and the server refuses an answer whose authenticator data lacks the user-verified flag, so the passkey alone stands for possession plus a fingerprint, face or PIN. The credential id is looked up for this relying party only; a user handle, when the browser sends one, must be the developer's (a SHA-256-derived handle, not the user name). The same one-time challenge, origin, RP ID hash, signature and counter checks as at the second step apply; failures are logged as `login_failed` with the developer's name when the key is known and count for the per-user and per-IP lockout. Administrators turn it off in Instance settings. A workspace's rule (administrators and its own administrators only, CSRF-checked) can only add a requirement; `pgkiln admin set-2fa-policy off` clears every workspace's rule. While a second factor is required of a developer, a sign-in with the default password leads only to the password change. |
116
118
 
117
119
  ## Findings of the MVP review and their fixes
118
120
 
@@ -235,4 +237,4 @@ A second full review, mainly of what end users and API callers can reach. Regres
235
237
  8. With REST data sources: list only the hosts you need in `PGKILN_REST_ALLOWED_HOSTS` (avoid `*`), set a long random `PGKILN_SECRET_KEY` (32+ characters) kept outside the database and its backups, and use `PGKILN_REST_PRIVATE_HOSTS` only for internal services you mean to reach. Narrow it further per workspace under Administration → Network access (sprint 118).
236
238
  9. With header authentication: list only your proxy's addresses in `PGKILN_AUTH_HEADER_PROXIES`, make sure users can't reach pgkiln directly, and have the proxy remove any incoming copy of the header.
237
239
  10. With database-account authentication: list exact roles or use a dedicated membership role, and limit what those roles may connect to in `pg_hba.conf` (they can reach the database with their password outside pgkiln too).
238
- 11. Turn on two-factor sign-in for the builder (Instance settings → Two-factor sign-in, at least for administrators) once two administrators have an authenticator app, keep `PGKILN_SECRET_KEY` backed up, and keep the recovery commands (`pgkiln admin reset-2fa`) for people with server access only.
240
+ 11. Turn on two-factor sign-in for the builder (Instance settings → Two-factor sign-in, at least for administrators) once two administrators have a passkey or an authenticator app; set `PUBLIC_URL` to the address developers use (passkeys belong to its host name), require it per workspace where the instance policy is too broad, keep *Trust a browser* short or at 0 on shared machines, keep `PGKILN_SECRET_KEY` backed up, and keep the recovery commands (`pgkiln admin reset-2fa`) for people with server access only.
@@ -0,0 +1,49 @@
1
+ -- ---------------------------------------------------------------------
2
+ -- 129: passkeys and trusted browsers for developers (sprint 127, SEC-2
3
+ -- part 2; beyond APEX).
4
+ --
5
+ -- meta.developer_factor gets a second kind, 'passkey' (WebAuthn): the
6
+ -- credential id (base64url, unique), its public key as a JWK, the COSE
7
+ -- algorithm, the signature counter, the relying party id it was made for
8
+ -- (the host name; a passkey only works on that domain) and the transports
9
+ -- the browser reported. A passkey keeps working without the instance key
10
+ -- (it is a public key). An authenticator app keeps its encrypted secret.
11
+ --
12
+ -- meta.developer_trusted_browser: "trust this browser" after a second
13
+ -- factor, for meta.setting mfa_trust_days days (0, the default: never).
14
+ -- Only the SHA-256 of the cookie's random token is stored; a reset, a new
15
+ -- password, removing a factor or "Forget trusted browsers" deletes them.
16
+ --
17
+ -- Owner-only, like meta.developer.
18
+ -- ---------------------------------------------------------------------
19
+ alter table meta.developer_factor drop constraint developer_factor_kind_check;
20
+ alter table meta.developer_factor
21
+ alter column secret_enc drop not null,
22
+ add column credential_id text unique,
23
+ add column public_key jsonb,
24
+ add column alg int,
25
+ add column sign_count bigint not null default 0,
26
+ add column rp_id text,
27
+ add column transports text[] not null default '{}',
28
+ add constraint developer_factor_kind_check check (kind in ('totp', 'passkey')),
29
+ add constraint developer_factor_kind_fields check (
30
+ case kind
31
+ when 'totp' then secret_enc is not null
32
+ when 'passkey' then credential_id is not null and public_key is not null and alg is not null and rp_id is not null
33
+ end);
34
+ comment on table meta.developer_factor is
35
+ 'A developer''s second factors: authenticator apps (TOTP, 128; the secret encrypted with the instance key) and passkeys (WebAuthn, 129; a public key).';
36
+
37
+ create table meta.developer_trusted_browser (
38
+ token_hash text primary key,
39
+ username text not null references meta.developer on update cascade on delete cascade,
40
+ created_at timestamptz not null default now(),
41
+ expires_at timestamptz not null,
42
+ user_agent text
43
+ );
44
+ create index on meta.developer_trusted_browser (username);
45
+ comment on table meta.developer_trusted_browser is
46
+ 'Browsers a developer trusted after a second factor (129): sha256 of the cookie token, until expires_at.';
47
+
48
+ insert into meta.setting (name, value) values ('mfa_trust_days', '0')
49
+ on conflict (name) do nothing;
@@ -0,0 +1,20 @@
1
+ -- ---------------------------------------------------------------------
2
+ -- 130: passkey sign-in and a workspace two-factor rule (sprint 128,
3
+ -- SEC-2 part 3; beyond APEX).
4
+ --
5
+ -- meta.workspace.mfa_required_since: when set, every developer of the
6
+ -- workspace needs a second factor, with the instance's grace period
7
+ -- (mfa_grace_days) counted from this moment; null = the instance policy
8
+ -- alone decides. Administrators of the instance are covered only where they
9
+ -- are members. `pgkiln admin set-2fa-policy off` clears it everywhere.
10
+ --
11
+ -- meta.setting mfa_passkey_signin ('on', the default, or 'off'): a
12
+ -- developer may sign in with a passkey alone, without the password, when
13
+ -- the authenticator verified them (fingerprint, face, PIN). It counts as
14
+ -- both factors.
15
+ -- ---------------------------------------------------------------------
16
+ alter table meta.workspace add column mfa_required_since timestamptz;
17
+
18
+ comment on column meta.workspace.mfa_required_since is 'Two-factor sign-in required for this workspace''s developers since (null = not by the workspace) (130).';
19
+
20
+ insert into meta.setting (name, value) values ('mfa_passkey_signin', 'on') on conflict (name) do nothing;
@@ -138,7 +138,7 @@ the 26.2 comparison.)
138
138
  | APEX accounts | ✅ | Instance-wide user directory, bcrypt, session rotation, idle and absolute timeouts |
139
139
  | Application Access Control (roles per app, any-user switch) | ✅ | Access control per application |
140
140
  | Social sign-in / OpenID Connect | ✅ | Any OIDC provider (Entra ID, Google, Okta, Keycloak, …): PKCE, group → role mapping, account linking, automatic accounts |
141
- | Multi-factor authentication | ✅ | As in APEX: through the identity provider (OpenID Connect, SAML). Beyond APEX (sprint 126): the builder itself has two-factor sign-in with authenticator apps (TOTP) and recovery codes, a policy with a grace period and lockout recovery ([chapter 3](guide/03-builder.md#two-factor-sign-in)); passkeys and two-factor for application users follow (SEC-2, SEC-3) |
141
+ | Multi-factor authentication | ✅ | As in APEX: through the identity provider (OpenID Connect, SAML). Beyond APEX (sprints 126–128): the builder itself has two-factor sign-in with passkeys (WebAuthn, also on their own without a password), authenticator apps (TOTP) and recovery codes, trusted browsers, an instance and per-workspace policy with a grace period and lockout recovery ([chapter 3](guide/03-builder.md#two-factor-sign-in)); two-factor for application users follows (SEC-3) |
142
142
  | Change own password | ✅ | A built-in *My account* page (APEX offers the API `CHANGE_CURRENT_USER_PW`) |
143
143
  | Password expiry, change on first use, admin reset, complexity | ✅ | Lifetime in days, expire/unexpire, admin reset, length and letters+digits rules, unlock |
144
144
  | Lockout after failed sign-ins | ✅ | Per user and per IP, time-based (APEX: until an administrator unlocks) |
@@ -296,8 +296,9 @@ Small but real differences, for teams comparing the two:
296
296
  - Responsive layouts verified automatically at four screen sizes.
297
297
  - The theme follows the device's light/dark setting automatically, and the choice is saved per user.
298
298
  - A built-in *My account* page (own password, theme, language).
299
- - (sprint 126) **Two-factor sign-in for the builder** without an identity provider: authenticator apps (TOTP),
300
- recovery codes, a policy for administrators or all developers with a grace period, and lockout recovery on the
299
+ - (sprints 126–128) **Two-factor sign-in for the builder** without an identity provider: passkeys (WebAuthn; sign-in with a
300
+ passkey alone), authenticator apps (TOTP), recovery codes, a policy for administrators, all developers or a
301
+ workspace's developers with a grace period, and lockout recovery on the
301
302
  command line ([chapter 3](guide/03-builder.md#two-factor-sign-in)). APEX relies on the identity provider.
302
303
  - One RLS policy set protects both the web UI and the REST API.
303
304
  - An import can be checked before it commits (`pgkiln import --check`): SQL that names a missing
@@ -141,7 +141,8 @@ which is read at startup; real environment variables take precedence.
141
141
  | `RUNTIME_DATABASE_URL` | *(falls back to `DATABASE_URL`)* | Least-privilege connection that runs applications |
142
142
  | `PORT` | `3100` | HTTP port |
143
143
  | `HOST` | `127.0.0.1` | Interface to listen on; use `0.0.0.0` in a container |
144
- | `PUBLIC_URL` | `http://127.0.0.1:<PORT>` | The address users reach pgkiln at (e.g. `https://apps.example.com`). Single sign-on redirect URIs are built from it |
144
+ | `PUBLIC_URL` | `http://127.0.0.1:<PORT>` | The address users reach pgkiln at (e.g. `https://apps.example.com`). Single sign-on redirect URIs are built from it, and developers' passkeys belong to its host name |
145
+ | `PGKILN_RP_ID` | the host of `PUBLIC_URL` | (0.61) A parent domain for developers' passkeys (`example.com` when the builder is at `builder.example.com`), so one passkey works on its sub-domains. Ignored unless it is the host or a parent of it |
145
146
  | `API_URL` | `http://127.0.0.1:3000` | Where PostgREST serves the REST API ([chapter 13](13-rest-api.md)) |
146
147
  | `API_JWT_SECRET` | *(none)* | Signs REST API tokens; at least 32 characters, the same as PostgREST's `jwt-secret`. Without it, tokens can't be issued |
147
148
  | `COOKIE_SECURE` | `false` | `true` behind HTTPS: marks cookies `Secure` and sends HSTS |
@@ -40,8 +40,9 @@ joins the workspace you are working in; one made with the command line or SQL jo
40
40
  ### Two-factor sign-in
41
41
 
42
42
  (0.60) Developers can sign in to the builder with a second factor: a 6-digit code from an **authenticator app**
43
- (Google Authenticator, Microsoft Authenticator, 1Password, Bitwarden, any app for TOTP codes per RFC 6238).
44
- APEX has no built-in second factor for the builder; there it comes from an identity provider.
43
+ (Google Authenticator, Microsoft Authenticator, 1Password, Bitwarden, any app for TOTP codes per RFC 6238) or
44
+ (0.61) a **passkey**: the fingerprint reader, face recognition or screen lock of a laptop or phone, or a security key
45
+ (WebAuthn). APEX has no built-in second factor for the builder; there it comes from an identity provider.
45
46
 
46
47
  **Setting it up.** Your account menu → **Two-factor sign-in** opens **Security** (`/builder/security`):
47
48
 
@@ -49,10 +50,30 @@ APEX has no built-in second factor for the builder; there it comes from an ident
49
50
  2. Enter the code the app shows, give the app a name (*Phone*) and **Confirm and turn on**.
50
51
  3. You get **ten recovery codes**, shown once. Save them away from your phone and tick *I saved these codes*.
51
52
 
52
- From then on, signing in asks for a code after the password. A recovery code works instead of an app code, once;
53
+ **A passkey** instead (or as well): on Security, give it a name (*Laptop*) and click **Add a passkey**; the browser asks
54
+ for your fingerprint, face, screen lock or security key. Add one on each device you use, or one that syncs (iCloud
55
+ Keychain, Google Password Manager, 1Password). The first factor you add, app or passkey, gives you the recovery codes.
56
+
57
+ From then on, signing in asks for a code, or a passkey with **Use a passkey**, after the password. A recovery code works instead of an app code, once;
53
58
  signing in with one opens Security, which shows how many are left and makes new ones (the old ones stop working).
54
- Removing an app asks for your password. Each code is accepted once, and one 30-second step either side of the
55
- server's clock.
59
+ Removing an app or a passkey asks for your password. Each code is accepted once, and one 30-second step either side
60
+ of the server's clock.
61
+
62
+ A passkey belongs to the **address** it was made at. With `PUBLIC_URL` set, that is its host name; otherwise the
63
+ host name in the browser's address bar. Passkeys need a host name: at `http://127.0.0.1:3100` the page asks you to
64
+ use `http://localhost:3100` instead. If the builder moves to another domain, passkeys made for the old one are
65
+ marked *for old.example.com* and stop working there; add new ones (apps and recovery codes keep working). To use
66
+ one passkey on several sub-domains, set `PGKILN_RP_ID` to the parent domain (`example.com` for
67
+ `builder.example.com`; [chapter 1](01-installation.md)). Passkeys are public keys: they keep working when the
68
+ instance key changes. pgkiln accepts any authenticator (attestation *none*), checks the signature, the address and
69
+ the relying party on every sign-in, and refuses a key whose signature counter goes back (a copied key).
70
+
71
+ **A passkey alone** (0.62): the sign-in page has **Sign in with a passkey** next to the password form. The browser
72
+ offers the passkeys it holds for this site, and you choose one; no user name, no password. It only works when the
73
+ device verified you (fingerprint, face or PIN), so the passkey counts as both factors; a workspace typed in the form
74
+ still applies. Administrators can turn it off in Instance settings → *Two-factor sign-in* (*Developers may sign in
75
+ with a passkey alone*, on by default). A security key without a PIN, or a passkey made before 0.61 that the browser
76
+ doesn't offer, still works at the second step after the password.
56
77
 
57
78
  The app's secret is stored encrypted with the instance key (`PGKILN_SECRET_KEY`, [chapter 1](01-installation.md));
58
79
  without the key the page says so and you can't add an app. Recovery codes are stored as SHA-256 hashes, so they keep
@@ -63,6 +84,21 @@ sign-in*: **Nobody** (optional, the default), **Administrators** or **All develo
63
84
  (default 7 days). During the grace period a developer without an app sees a reminder on every page; after it, they
64
85
  see only their Security page until they add one. The region lists the developers who have no app yet.
65
86
 
87
+ **Per workspace** (0.62): a workspace's page (Workspace utilities → Workspaces → the workspace, *Preferences*) has
88
+ *Require two-factor sign-in of this workspace's developers*, for administrators and the workspace's own
89
+ administrators. It adds to the instance's policy: the workspace's developers get the instance's grace period from the
90
+ moment it was turned on, then only their Security page. The page lists the members who have no factor yet.
91
+
92
+ **The default password** (0.62): a developer who signs in with the default password (`admin`, or their user name)
93
+ while two-factor sign-in is required of them sees only **Developers**, to change it, before anything else (then the
94
+ grace period or the Security page as above).
95
+
96
+ **Trusted browsers** (0.61): *Trust a browser (days)* in the same region (0, the default: never) adds *Trust this
97
+ browser for N days* to the second step. A trusted browser skips the second step for that developer until the days
98
+ are over; only a hash of the cookie's random token is stored. The developer's Security page shows how many browsers
99
+ they trust and **Forget trusted browsers**; changing the password, removing a factor or a reset forgets them too, and
100
+ setting the days to 0 stops every trusted browser at once.
101
+
66
102
  **So nobody is locked out:**
67
103
 
68
104
  - The policy can be turned on only when **at least two administrators** have two-factor sign-in (or every
@@ -79,10 +115,11 @@ see only their Security page until they add one. The region lists the developers
79
115
 
80
116
  | Situation | What to do |
81
117
  |---|---|
82
- | Lost your phone, still have recovery codes | Sign in with a recovery code, remove the old app on Security and add the new phone |
83
- | Lost phone and codes | Ask another administrator to reset your two-factor sign-in on Developers |
118
+ | Lost your phone or key, still have recovery codes | Sign in with a recovery code, remove the old app or passkey on Security and add the new one |
119
+ | Lost phone, keys and codes | Ask another administrator to reset your two-factor sign-in on Developers |
120
+ | A passkey no longer works after a move to another domain | Sign in with an app code or a recovery code and add a new passkey ([above](#two-factor-sign-in)) |
84
121
  | No administrator can sign in | On the server: `pgkiln admin reset-2fa <developer> --reason "…"` ([chapter 18](18-cli.md)), then sign in with the password |
85
- | The policy locks everyone out | On the server: `pgkiln admin set-2fa-policy off` |
122
+ | The policy locks everyone out | On the server: `pgkiln admin set-2fa-policy off` (also clears the workspaces' own rules) |
86
123
  | Codes are always wrong | Check the time on the phone and on the server (codes allow 30 seconds either way) |
87
124
  | The instance key was lost or changed | The apps' secrets can't be read: reset the developers (`pgkiln admin reset-2fa`); recovery codes still work |
88
125
 
@@ -53,7 +53,7 @@ any (see [what is not exported](03-builder.md#export-format)).
53
53
  | `pgkiln users add <username> [--developer] [--app <alias> --roles a,b] [--name …] [--email …]` | Adds an account (optionally with access to an application) or a builder developer |
54
54
  | `pgkiln users password <username> [--developer]` | Sets a password and ends that user's sessions |
55
55
  | `pgkiln admin reset-2fa <developer> [--reason <text>]` | (0.60) Removes a developer's authenticator apps and recovery codes and ends their builder sessions: [locked out?](03-builder.md#locked-out) |
56
- | `pgkiln admin set-2fa-policy off\|admins\|all [--grace <days>]` | (0.60) Sets the builder's [two-factor policy](03-builder.md#two-factor-sign-in); `off` always works, the others need two administrators with an app |
56
+ | `pgkiln admin set-2fa-policy off\|admins\|all [--grace <days>]` | (0.60) Sets the builder's [two-factor policy](03-builder.md#two-factor-sign-in); `off` always works (and, 0.62, clears every workspace's own rule), the others need two administrators with an app |
57
57
 
58
58
  `users` reads the password from standard input (the first line, e.g. from a secret store:
59
59
  `printf '%s\n' "$PW" | pgkiln users add ops --developer`) or asks for it twice on a terminal. It is
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "pgkiln",
3
- "version": "0.60.0",
3
+ "version": "0.62.0",
4
4
  "description": "Open source low-code, SQL-driven application builder for PostgreSQL, inspired by Oracle APEX",
5
5
  "keywords": [
6
6
  "postgresql",
@@ -675,6 +675,7 @@ kbd { display: inline-block; padding: 0 0.35em; border: 1px solid var(--border);
675
675
  .signin-reveal { flex: none; border: 0; background: none; color: var(--accent); cursor: pointer; font-size: 0.85rem; padding: 0.25rem; }
676
676
  .signin-remember { font-size: 0.9rem; }
677
677
  .signin-submit { padding: 0.75rem; font-size: 1rem; border-radius: var(--radius); }
678
+ [data-passkey-status]:empty { display: none; }
678
679
  .signin-hint { margin: 0; text-align: center; }
679
680
  .signin-actions { display: flex; flex-wrap: wrap; justify-content: center; gap: 0.5rem 1.5rem; margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--border); }
680
681
  .signin-actions a { display: inline-flex; align-items: center; gap: 0.4rem; color: var(--gold); text-decoration: none; }
package/public/builder.js CHANGED
@@ -1333,3 +1333,66 @@ document.querySelectorAll('input[data-filter-tree]').forEach((el) => {
1333
1333
  });
1334
1334
  }
1335
1335
  })();
1336
+
1337
+ // (sprint 127) Passkeys: a [data-passkey] button asks the server (its URL) for WebAuthn options, calls
1338
+ // navigator.credentials.create() (Security page) or .get() (the second sign-in step) and posts the answer in
1339
+ // its own form, to data-passkey-submit when set, so the server answers with a redirect as for any form.
1340
+ // (sprint 128) On the sign-in page the list of allowed passkeys is empty: the browser offers the ones it holds.
1341
+ // Without WebAuthn or JavaScript the buttons stay hidden; codes keep working.
1342
+ (() => {
1343
+ 'use strict';
1344
+ if (!window.PublicKeyCredential || !navigator.credentials) return;
1345
+ const b64 = (buf) => btoa(String.fromCharCode(...new Uint8Array(buf))).replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
1346
+ const bytes = (s) => Uint8Array.from(atob(s.replace(/-/g, '+').replace(/_/g, '/') + '==='.slice((s.length + 3) % 4)), (c) => c.charCodeAt(0));
1347
+ for (const btn of document.querySelectorAll('[data-passkey]')) {
1348
+ const form = btn.closest('form');
1349
+ if (!form) continue;
1350
+ btn.hidden = false;
1351
+ btn.addEventListener('click', async (e) => {
1352
+ e.preventDefault();
1353
+ const status = form.querySelector('[data-passkey-status]');
1354
+ const set = (name, value) => {
1355
+ const field = form.querySelector(`[name="${name}"]`);
1356
+ if (field) field.value = value;
1357
+ };
1358
+ btn.disabled = true;
1359
+ if (status) status.textContent = '';
1360
+ try {
1361
+ const res = await fetch(btn.dataset.passkey, {
1362
+ method: 'POST', credentials: 'same-origin', headers: { accept: 'application/json' },
1363
+ body: new URLSearchParams({ __csrf: form.querySelector('[name="__csrf"]').value }),
1364
+ });
1365
+ if (!res.ok) throw new Error(String(res.status));
1366
+ const o = await res.json();
1367
+ if (o.mode === 'create') {
1368
+ const c = await navigator.credentials.create({ publicKey: {
1369
+ challenge: bytes(o.challenge), rp: o.rp,
1370
+ user: { id: bytes(o.user.id), name: o.user.name, displayName: o.user.displayName },
1371
+ pubKeyCredParams: o.algs.map((alg) => ({ type: 'public-key', alg })),
1372
+ excludeCredentials: o.exclude.map((id) => ({ type: 'public-key', id: bytes(id) })),
1373
+ authenticatorSelection: { residentKey: 'preferred', userVerification: 'preferred' },
1374
+ attestation: 'none', timeout: 120000,
1375
+ } });
1376
+ set('clientDataJSON', b64(c.response.clientDataJSON));
1377
+ set('attestationObject', b64(c.response.attestationObject));
1378
+ set('transports', (c.response.getTransports ? c.response.getTransports() : []).join(','));
1379
+ } else {
1380
+ const c = await navigator.credentials.get({ publicKey: {
1381
+ challenge: bytes(o.challenge), rpId: o.rpId, userVerification: o.uv || 'preferred', timeout: 120000,
1382
+ allowCredentials: o.allow.map((a) => ({ type: 'public-key', id: bytes(a.id), transports: a.transports })),
1383
+ } });
1384
+ set('credentialId', b64(c.rawId));
1385
+ set('clientDataJSON', b64(c.response.clientDataJSON));
1386
+ set('authenticatorData', b64(c.response.authenticatorData));
1387
+ set('signature', b64(c.response.signature));
1388
+ if (c.response.userHandle) set('userHandle', b64(c.response.userHandle));
1389
+ }
1390
+ if (btn.dataset.passkeySubmit) form.action = btn.dataset.passkeySubmit;
1391
+ form.submit();
1392
+ } catch (err) {
1393
+ btn.disabled = false;
1394
+ if (status) status.textContent = btn.dataset.passkeyError || String(err);
1395
+ }
1396
+ });
1397
+ }
1398
+ })();
@@ -1714,16 +1714,16 @@ export const de: BuilderMessages = {
1714
1714
  // ------------------------------------------------------------ Security: two-factor sign-in (sprint 126)
1715
1715
  'mfa.menu': 'Zwei-Faktor-Anmeldung',
1716
1716
  'mfa.title': 'Sicherheit',
1717
- 'mfa.intro': 'Melden Sie sich nach dem Passwort mit einem Code aus einer Authenticator-App an. Wiederherstellungscodes helfen, wenn Sie das Telefon verlieren.',
1718
- 'mfa.required_you': 'Ihr Administrator verlangt die Zwei-Faktor-Anmeldung: Fügen Sie eine Authenticator-App hinzu.',
1717
+ 'mfa.intro': 'Bestätigen Sie nach dem Passwort mit einem Passkey (Fingerabdruck, Gesicht, Sicherheitsschlüssel) oder einem Code aus einer Authenticator-App, dass Sie es sind. Wiederherstellungscodes helfen, wenn Sie diese verlieren.',
1718
+ 'mfa.required_you': 'Ihr Administrator verlangt die Zwei-Faktor-Anmeldung: Fügen Sie einen Passkey oder eine Authenticator-App hinzu.',
1719
1719
  'mfa.banner': 'Die Zwei-Faktor-Anmeldung ist erforderlich. Richten Sie sie vor dem {date} ein: {link}',
1720
1720
  'mfa.set_up_now': 'jetzt einrichten',
1721
- 'mfa.apps': 'Authenticator-Apps',
1721
+ 'mfa.factors': 'Passkeys und Authenticator-Apps',
1722
1722
  'mfa.app_name': 'Name',
1723
1723
  'mfa.app_default': 'Telefon',
1724
1724
  'mfa.added': 'Hinzugefügt',
1725
1725
  'mfa.last_used': 'Zuletzt verwendet',
1726
- 'mfa.no_apps': 'Noch keine Authenticator-App: Sie melden sich nur mit Ihrem Passwort an.',
1726
+ 'mfa.no_apps': 'Noch kein zweiter Faktor: Sie melden sich nur mit Ihrem Passwort an.',
1727
1727
  'mfa.add_app': 'Authenticator-App hinzufügen',
1728
1728
  'mfa.no_key': 'Die Zwei-Faktor-Anmeldung braucht den Instanzschlüssel (PGKILN_SECRET_KEY), um das Geheimnis der App zu speichern.',
1729
1729
  'mfa.scan': 'Scannen Sie den QR-Code mit einer Authenticator-App.',
@@ -1739,28 +1739,28 @@ export const de: BuilderMessages = {
1739
1739
  'mfa.remove_app': 'Diese App entfernen',
1740
1740
  'mfa.your_password': 'Ihr Passwort',
1741
1741
  'mfa.wrong_password': 'Falsches Passwort.',
1742
- 'mfa.cannot_remove_required': 'Die Zwei-Faktor-Anmeldung ist für Sie erforderlich: Fügen Sie eine weitere App hinzu, bevor Sie diese entfernen.',
1742
+ 'mfa.cannot_remove_required': 'Die Zwei-Faktor-Anmeldung ist für Sie erforderlich: Fügen Sie einen weiteren Passkey oder eine App hinzu, bevor Sie diesen Faktor entfernen.',
1743
1743
  'mfa.cannot_remove_admin': 'Mindestens zwei Administratoren (oder alle, wenn es weniger sind) müssen die Zwei-Faktor-Anmeldung behalten, solange die Richtlinie gilt.',
1744
1744
  'mfa.keep_admins': 'Jetzt nicht: Die Zwei-Faktor-Richtlinie braucht mindestens zwei Administratoren mit Zwei-Faktor-Anmeldung (oder alle, wenn es weniger sind).',
1745
- 'mfa.removed': 'Authenticator-App entfernt.',
1745
+ 'mfa.removed': 'Entfernt.',
1746
1746
  'mfa.codes_title': 'Wiederherstellungscodes',
1747
1747
  'mfa.codes_once': 'Bewahren Sie diese Codes sicher und getrennt vom Telefon auf. Jeder Code ersetzt einmal einen App-Code. Sie werden nur jetzt angezeigt.',
1748
1748
  'mfa.codes_saved': 'Ich habe diese Codes gespeichert',
1749
1749
  'mfa.codes_done': 'Fertig',
1750
1750
  'mfa.codes_kept': 'Wiederherstellungscodes gespeichert.',
1751
1751
  'mfa.codes_left': 'Unbenutzte Wiederherstellungscodes: {n}.',
1752
- 'mfa.codes_after': 'Sie erhalten Wiederherstellungscodes, wenn Sie eine Authenticator-App hinzufügen.',
1752
+ 'mfa.codes_after': 'Sie erhalten Wiederherstellungscodes, wenn Sie einen Passkey oder eine Authenticator-App hinzufügen.',
1753
1753
  'mfa.codes_low': 'Nur noch wenige Wiederherstellungscodes: Erstellen Sie neue.',
1754
1754
  'mfa.codes_new': 'Neue Wiederherstellungscodes',
1755
1755
  'mfa.codes_new_confirm': 'Neue Wiederherstellungscodes erstellen? Die alten gelten dann nicht mehr.',
1756
1756
  'mfa.expired': 'Die Anmeldung hat zu lange gedauert: Melden Sie sich erneut an.',
1757
1757
  'mfa.step_title': 'Zwei-Faktor-Anmeldung',
1758
- 'mfa.step_intro': 'Geben Sie den Code aus Ihrer Authenticator-App für {user} ein.',
1758
+ 'mfa.step_intro': 'Bestätigen Sie, dass Sie es sind, {user}: mit einem Passkey oder einem Code aus Ihrer Authenticator-App.',
1759
1759
  'mfa.code': 'Code',
1760
1760
  'mfa.code_help': '6 Ziffern aus der App oder einer Ihrer Wiederherstellungscodes.',
1761
1761
  'mfa.verify': 'Bestätigen',
1762
1762
  'mfa.cancel': 'Zurück zur Anmeldung',
1763
- 'mfa.locked_out': 'Telefon und Codes verloren? Bitten Sie einen Administrator, Ihre Zwei-Faktor-Anmeldung zurückzusetzen.',
1763
+ 'mfa.locked_out': 'Telefon, Schlüssel und Codes verloren? Bitten Sie einen Administrator, Ihre Zwei-Faktor-Anmeldung zurückzusetzen.',
1764
1764
  'mfa.wrong': 'Dieser Code ist falsch oder wurde schon verwendet.',
1765
1765
  'mfa.column': 'Zwei-Faktor',
1766
1766
  'mfa.on': 'an',
@@ -1784,4 +1784,40 @@ export const de: BuilderMessages = {
1784
1784
  'mfa.policy_needs_admins': 'Zuerst müssen mindestens zwei Administratoren (oder alle, wenn es weniger sind) die Zwei-Faktor-Anmeldung einrichten, damit niemand allein ausgesperrt wird.',
1785
1785
  'mfa.without': 'Ohne Zwei-Faktor-Anmeldung: {list}.',
1786
1786
  'mfa.cli_note': 'Ausgesperrt? Auf dem Server: pgkiln admin reset-2fa <developer> oder pgkiln admin set-2fa-policy off.',
1787
+ // ------------------------------------------------------------ Security: passkeys and trusted browsers (sprint 127)
1788
+ 'mfa.kind': 'Art',
1789
+ 'mfa.kind_passkey': 'Passkeys',
1790
+ 'mfa.kind_app': 'Authenticator-Apps',
1791
+ 'mfa.other_domain': 'für {domain}',
1792
+ 'mfa.other_domain_help': 'Für {domain} erstellt: Unter dieser Adresse funktioniert er nicht. Fügen Sie hier einen neuen Passkey hinzu.',
1793
+ 'mfa.passkey_intro': 'Melden Sie sich mit Fingerabdruck, Gesicht, Bildschirmsperre oder einem Sicherheitsschlüssel an. Ein Passkey funktioniert nur auf dieser Website.',
1794
+ 'mfa.passkey_default': 'Passkey',
1795
+ 'mfa.add_passkey': 'Passkey hinzufügen',
1796
+ 'mfa.passkey_needs_js': 'Zum Hinzufügen eines Passkeys ist JavaScript nötig; eine Authenticator-App funktioniert ohne.',
1797
+ 'mfa.passkey_failed': 'Der Passkey hat nicht funktioniert. Versuchen Sie es erneut oder verwenden Sie einen Code.',
1798
+ 'mfa.passkey_known': 'Dieser Passkey ist bereits hinzugefügt.',
1799
+ 'mfa.passkey_added': 'Passkey hinzugefügt: Ab jetzt können Sie sich damit anmelden.',
1800
+ 'mfa.remove_passkey': 'Diesen Passkey entfernen',
1801
+ 'mfa.use_passkey': 'Passkey verwenden',
1802
+ 'mfa.trust_browser': 'Diesem Browser {days} Tage vertrauen',
1803
+ 'mfa.trusted_title': 'Vertrauenswürdige Browser',
1804
+ 'mfa.trusted_count': 'Browser, die den zweiten Schritt überspringen: {n} (höchstens {days} Tage).',
1805
+ 'mfa.trusted_off': 'Das Vertrauen von Browsern ist ausgeschaltet.',
1806
+ 'mfa.forget_browsers': 'Vertrauenswürdige Browser vergessen',
1807
+ 'mfa.browsers_forgotten': 'Jeder Browser fragt wieder nach dem zweiten Schritt.',
1808
+ 'mfa.trust_days': 'Browser vertrauen (Tage)',
1809
+ 'mfa.trust_days_help': 'Nach dem zweiten Schritt darf ein Entwickler ihn in diesem Browser so viele Tage überspringen. 0: nie.',
1810
+ 'mfa.passkey_needs_host': 'Passkeys brauchen einen Hostnamen, keine IP-Adresse ({host}): Öffnen Sie den Builder über localhost oder seinen Domainnamen.',
1811
+ 'signin.passkey': 'Mit einem Passkey anmelden',
1812
+ 'mfa.passkey_signin': 'Entwickler dürfen sich nur mit einem Passkey anmelden (ohne Passwort)',
1813
+ 'mfa.passkey_signin_help': 'Nur wenn das Gerät den Entwickler geprüft hat (Fingerabdruck, Gesicht oder PIN): Dann zählt der Passkey als beide Faktoren.',
1814
+ 'mfa.weak_required': 'Zwei-Faktor-Anmeldung ist erforderlich: Ändern Sie das Standardpasswort, bevor Sie fortfahren.',
1815
+ 'wsp.mfa': 'Zwei-Faktor-Anmeldung',
1816
+ 'wsp.mfa_required': 'Zwei-Faktor-Anmeldung für die Entwickler dieses Workspace verlangen',
1817
+ 'wsp.mfa_required_help': 'Zusätzlich zur Richtlinie der Instanz. Entwickler ohne Passkey oder Authenticator-App erhalten die Übergangsfrist der Instanz, danach nur ihre Sicherheitsseite.',
1818
+ 'wsp.mfa_since': 'Erforderlich seit {date}.',
1819
+ 'wsp.mfa_without': 'Noch nicht eingerichtet: {list}.',
1820
+ 'wsp.mfa_save': 'Zwei-Faktor-Regel speichern',
1821
+ 'wsp.mfa_on': 'Die Entwickler dieses Workspace brauchen die Zwei-Faktor-Anmeldung.',
1822
+ 'wsp.mfa_off': 'Der Workspace verlangt keine Zwei-Faktor-Anmeldung mehr (die Richtlinie der Instanz gilt weiter).',
1787
1823
  };
@@ -1717,16 +1717,16 @@ export const en = {
1717
1717
  // ------------------------------------------------------------ Security: two-factor sign-in (sprint 126)
1718
1718
  'mfa.menu': 'Two-factor sign-in',
1719
1719
  'mfa.title': 'Security',
1720
- 'mfa.intro': 'Sign in with a code from an authenticator app after your password. Recovery codes get you in when you lose the phone.',
1721
- 'mfa.required_you': 'Your administrator requires two-factor sign-in: add an authenticator app.',
1720
+ 'mfa.intro': 'After your password, confirm it’s you with a passkey (fingerprint, face, security key) or a code from an authenticator app. Recovery codes get you in when you lose them.',
1721
+ 'mfa.required_you': 'Your administrator requires two-factor sign-in: add a passkey or an authenticator app.',
1722
1722
  'mfa.banner': 'Two-factor sign-in is required. Set it up before {date}: {link}',
1723
1723
  'mfa.set_up_now': 'set up now',
1724
- 'mfa.apps': 'Authenticator apps',
1724
+ 'mfa.factors': 'Passkeys and authenticator apps',
1725
1725
  'mfa.app_name': 'Name',
1726
1726
  'mfa.app_default': 'Phone',
1727
1727
  'mfa.added': 'Added',
1728
1728
  'mfa.last_used': 'Last used',
1729
- 'mfa.no_apps': 'No authenticator app yet: you sign in with your password only.',
1729
+ 'mfa.no_apps': 'No second factor yet: you sign in with your password only.',
1730
1730
  'mfa.add_app': 'Add an authenticator app',
1731
1731
  'mfa.no_key': 'Two-factor sign-in needs the instance key (PGKILN_SECRET_KEY) to store the app’s secret.',
1732
1732
  'mfa.scan': 'Scan the QR code with an authenticator app.',
@@ -1742,28 +1742,28 @@ export const en = {
1742
1742
  'mfa.remove_app': 'Remove this app',
1743
1743
  'mfa.your_password': 'Your password',
1744
1744
  'mfa.wrong_password': 'Wrong password.',
1745
- 'mfa.cannot_remove_required': 'Two-factor sign-in is required for you: add another app before you remove this one.',
1745
+ 'mfa.cannot_remove_required': 'Two-factor sign-in is required for you: add another passkey or app before you remove this one.',
1746
1746
  'mfa.cannot_remove_admin': 'At least two administrators (or every administrator, when there are fewer) must keep two-factor sign-in while the policy is on.',
1747
1747
  'mfa.keep_admins': 'Not now: the two-factor policy needs at least two administrators with two-factor sign-in (or every administrator, when there are fewer).',
1748
- 'mfa.removed': 'Authenticator app removed.',
1748
+ 'mfa.removed': 'Removed.',
1749
1749
  'mfa.codes_title': 'Recovery codes',
1750
1750
  'mfa.codes_once': 'Save these codes somewhere safe, away from your phone. Each one signs you in once instead of an app code. They are shown only now.',
1751
1751
  'mfa.codes_saved': 'I saved these codes',
1752
1752
  'mfa.codes_done': 'Done',
1753
1753
  'mfa.codes_kept': 'Recovery codes saved.',
1754
1754
  'mfa.codes_left': 'Unused recovery codes: {n}.',
1755
- 'mfa.codes_after': 'You get recovery codes when you add an authenticator app.',
1755
+ 'mfa.codes_after': 'You get recovery codes when you add a passkey or an authenticator app.',
1756
1756
  'mfa.codes_low': 'Few recovery codes left: make new ones.',
1757
1757
  'mfa.codes_new': 'New recovery codes',
1758
1758
  'mfa.codes_new_confirm': 'Make new recovery codes? The old ones stop working.',
1759
1759
  'mfa.expired': 'The sign-in took too long: sign in again.',
1760
1760
  'mfa.step_title': 'Two-factor sign-in',
1761
- 'mfa.step_intro': 'Enter the code from your authenticator app for {user}.',
1761
+ 'mfa.step_intro': 'Confirm it’s you, {user}: use a passkey or enter a code from your authenticator app.',
1762
1762
  'mfa.code': 'Code',
1763
1763
  'mfa.code_help': '6 digits from the app, or one of your recovery codes.',
1764
1764
  'mfa.verify': 'Verify',
1765
1765
  'mfa.cancel': 'Back to sign-in',
1766
- 'mfa.locked_out': 'Lost your phone and codes? Ask an administrator to reset your two-factor sign-in.',
1766
+ 'mfa.locked_out': 'Lost your phone, key and codes? Ask an administrator to reset your two-factor sign-in.',
1767
1767
  'mfa.wrong': 'That code is not right, or was already used.',
1768
1768
  'mfa.column': 'Two-factor',
1769
1769
  'mfa.on': 'on',
@@ -1787,4 +1787,40 @@ export const en = {
1787
1787
  'mfa.policy_needs_admins': 'First, at least two administrators (or every administrator, when there are fewer) set up two-factor sign-in, so nobody is locked out alone.',
1788
1788
  'mfa.without': 'Without two-factor sign-in: {list}.',
1789
1789
  'mfa.cli_note': 'Locked out? On the server: pgkiln admin reset-2fa <developer> or pgkiln admin set-2fa-policy off.',
1790
+ // ------------------------------------------------------------ Security: passkeys and trusted browsers (sprint 127)
1791
+ 'mfa.kind': 'Kind',
1792
+ 'mfa.kind_passkey': 'Passkeys',
1793
+ 'mfa.kind_app': 'Authenticator apps',
1794
+ 'mfa.other_domain': 'for {domain}',
1795
+ 'mfa.other_domain_help': 'Made for {domain}: it doesn’t work at this address. Add a new passkey here.',
1796
+ 'mfa.passkey_intro': 'Sign in with your fingerprint, face or screen lock, or a security key. A passkey only works on this site.',
1797
+ 'mfa.passkey_default': 'Passkey',
1798
+ 'mfa.add_passkey': 'Add a passkey',
1799
+ 'mfa.passkey_needs_js': 'Adding a passkey needs JavaScript; an authenticator app works without it.',
1800
+ 'mfa.passkey_failed': 'The passkey didn’t work. Try again, or use a code.',
1801
+ 'mfa.passkey_known': 'This passkey is already added.',
1802
+ 'mfa.passkey_added': 'Passkey added: from now on you can sign in with it.',
1803
+ 'mfa.remove_passkey': 'Remove this passkey',
1804
+ 'mfa.use_passkey': 'Use a passkey',
1805
+ 'mfa.trust_browser': 'Trust this browser for {days} days',
1806
+ 'mfa.trusted_title': 'Trusted browsers',
1807
+ 'mfa.trusted_count': 'Browsers that skip the second step: {n} (for up to {days} days).',
1808
+ 'mfa.trusted_off': 'Trusting browsers is turned off.',
1809
+ 'mfa.forget_browsers': 'Forget trusted browsers',
1810
+ 'mfa.browsers_forgotten': 'Every browser asks for the second step again.',
1811
+ 'mfa.trust_days': 'Trust a browser (days)',
1812
+ 'mfa.trust_days_help': 'After the second step a developer may skip it in that browser for this many days. 0: never.',
1813
+ 'mfa.passkey_needs_host': 'Passkeys need a host name, not an IP address ({host}): open the builder at localhost or its domain name.',
1814
+ 'signin.passkey': 'Sign in with a passkey',
1815
+ 'mfa.passkey_signin': 'Developers may sign in with a passkey alone (no password)',
1816
+ 'mfa.passkey_signin_help': 'Only when the device checked the developer (fingerprint, face or PIN): the passkey then counts as both factors.',
1817
+ 'mfa.weak_required': 'Two-factor sign-in is required: change the default password before you continue.',
1818
+ 'wsp.mfa': 'Two-factor sign-in',
1819
+ 'wsp.mfa_required': 'Require two-factor sign-in of this workspace’s developers',
1820
+ 'wsp.mfa_required_help': 'On top of the instance’s policy. Developers without a passkey or authenticator app get the instance’s grace period, then only their Security page.',
1821
+ 'wsp.mfa_since': 'Required since {date}.',
1822
+ 'wsp.mfa_without': 'Not set up yet: {list}.',
1823
+ 'wsp.mfa_save': 'Save two-factor rule',
1824
+ 'wsp.mfa_on': 'This workspace’s developers need two-factor sign-in.',
1825
+ 'wsp.mfa_off': 'The workspace no longer requires two-factor sign-in (the instance’s policy still applies).',
1790
1826
  } as const;
@@ -1714,16 +1714,16 @@ export const es: BuilderMessages = {
1714
1714
  // ------------------------------------------------------------ Security: two-factor sign-in (sprint 126)
1715
1715
  'mfa.menu': 'Inicio de sesión en dos pasos',
1716
1716
  'mfa.title': 'Seguridad',
1717
- 'mfa.intro': 'Inicie sesión con un código de una aplicación de autenticación después de su contraseña. Los códigos de recuperación le permiten entrar si pierde el teléfono.',
1718
- 'mfa.required_you': 'Su administrador exige el inicio de sesión en dos pasos: añada una aplicación de autenticación.',
1717
+ 'mfa.intro': 'Después de su contraseña, confirme que es usted con una llave de acceso (huella, rostro, llave de seguridad) o un código de una aplicación de autenticación. Los códigos de recuperación le permiten entrar si los pierde.',
1718
+ 'mfa.required_you': 'Su administrador exige el inicio de sesión en dos pasos: añada una llave de acceso o una aplicación de autenticación.',
1719
1719
  'mfa.banner': 'El inicio de sesión en dos pasos es obligatorio. Configúrelo antes del {date}: {link}',
1720
1720
  'mfa.set_up_now': 'configurar ahora',
1721
- 'mfa.apps': 'Aplicaciones de autenticación',
1721
+ 'mfa.factors': 'Llaves de acceso y aplicaciones de autenticación',
1722
1722
  'mfa.app_name': 'Nombre',
1723
1723
  'mfa.app_default': 'Teléfono',
1724
1724
  'mfa.added': 'Añadida',
1725
1725
  'mfa.last_used': 'Último uso',
1726
- 'mfa.no_apps': 'Aún no hay aplicación de autenticación: inicia sesión solo con su contraseña.',
1726
+ 'mfa.no_apps': 'Aún no hay segundo factor: inicia sesión solo con su contraseña.',
1727
1727
  'mfa.add_app': 'Añadir una aplicación de autenticación',
1728
1728
  'mfa.no_key': 'El inicio de sesión en dos pasos necesita la clave de la instancia (PGKILN_SECRET_KEY) para guardar el secreto de la aplicación.',
1729
1729
  'mfa.scan': 'Escanee el código QR con una aplicación de autenticación.',
@@ -1739,28 +1739,28 @@ export const es: BuilderMessages = {
1739
1739
  'mfa.remove_app': 'Quitar esta aplicación',
1740
1740
  'mfa.your_password': 'Su contraseña',
1741
1741
  'mfa.wrong_password': 'Contraseña incorrecta.',
1742
- 'mfa.cannot_remove_required': 'El inicio de sesión en dos pasos es obligatorio para usted: añada otra aplicación antes de quitar esta.',
1742
+ 'mfa.cannot_remove_required': 'El inicio de sesión en dos pasos es obligatorio para usted: añada otra llave o aplicación antes de quitar esta.',
1743
1743
  'mfa.cannot_remove_admin': 'Al menos dos administradores (o todos, si hay menos) deben mantener el inicio de sesión en dos pasos mientras la política esté activa.',
1744
1744
  'mfa.keep_admins': 'Ahora no: la política de dos pasos necesita al menos dos administradores con inicio de sesión en dos pasos (o todos, si hay menos).',
1745
- 'mfa.removed': 'Aplicación de autenticación quitada.',
1745
+ 'mfa.removed': 'Quitado.',
1746
1746
  'mfa.codes_title': 'Códigos de recuperación',
1747
1747
  'mfa.codes_once': 'Guarde estos códigos en un lugar seguro, lejos del teléfono. Cada uno sustituye una vez a un código de la aplicación. Solo se muestran ahora.',
1748
1748
  'mfa.codes_saved': 'He guardado estos códigos',
1749
1749
  'mfa.codes_done': 'Hecho',
1750
1750
  'mfa.codes_kept': 'Códigos de recuperación guardados.',
1751
1751
  'mfa.codes_left': 'Códigos de recuperación sin usar: {n}.',
1752
- 'mfa.codes_after': 'Recibe códigos de recuperación al añadir una aplicación de autenticación.',
1752
+ 'mfa.codes_after': 'Recibe códigos de recuperación al añadir una llave de acceso o una aplicación de autenticación.',
1753
1753
  'mfa.codes_low': 'Quedan pocos códigos de recuperación: genere nuevos.',
1754
1754
  'mfa.codes_new': 'Nuevos códigos de recuperación',
1755
1755
  'mfa.codes_new_confirm': '¿Generar nuevos códigos de recuperación? Los antiguos dejan de funcionar.',
1756
1756
  'mfa.expired': 'El inicio de sesión tardó demasiado: inicie sesión de nuevo.',
1757
1757
  'mfa.step_title': 'Inicio de sesión en dos pasos',
1758
- 'mfa.step_intro': 'Introduzca el código de su aplicación de autenticación para {user}.',
1758
+ 'mfa.step_intro': 'Confirme que es usted, {user}: use una llave de acceso o introduzca un código de su aplicación de autenticación.',
1759
1759
  'mfa.code': 'Código',
1760
1760
  'mfa.code_help': '6 dígitos de la aplicación o uno de sus códigos de recuperación.',
1761
1761
  'mfa.verify': 'Verificar',
1762
1762
  'mfa.cancel': 'Volver al inicio de sesión',
1763
- 'mfa.locked_out': '¿Perdió el teléfono y los códigos? Pida a un administrador que restablezca su inicio de sesión en dos pasos.',
1763
+ 'mfa.locked_out': '¿Perdió el teléfono, la llave y los códigos? Pida a un administrador que restablezca su inicio de sesión en dos pasos.',
1764
1764
  'mfa.wrong': 'Ese código no es correcto o ya se usó.',
1765
1765
  'mfa.column': 'Dos pasos',
1766
1766
  'mfa.on': 'activado',
@@ -1784,4 +1784,40 @@ export const es: BuilderMessages = {
1784
1784
  'mfa.policy_needs_admins': 'Primero, al menos dos administradores (o todos, si hay menos) deben configurar el inicio de sesión en dos pasos, para que nadie quede bloqueado solo.',
1785
1785
  'mfa.without': 'Sin inicio de sesión en dos pasos: {list}.',
1786
1786
  'mfa.cli_note': '¿Bloqueado? En el servidor: pgkiln admin reset-2fa <developer> o pgkiln admin set-2fa-policy off.',
1787
+ // ------------------------------------------------------------ Security: passkeys and trusted browsers (sprint 127)
1788
+ 'mfa.kind': 'Tipo',
1789
+ 'mfa.kind_passkey': 'Llaves de acceso',
1790
+ 'mfa.kind_app': 'Aplicaciones de autenticación',
1791
+ 'mfa.other_domain': 'para {domain}',
1792
+ 'mfa.other_domain_help': 'Creada para {domain}: no funciona en esta dirección. Añada aquí una nueva llave.',
1793
+ 'mfa.passkey_intro': 'Inicie sesión con su huella, su rostro, el bloqueo de pantalla o una llave de seguridad. Una llave de acceso solo funciona en este sitio.',
1794
+ 'mfa.passkey_default': 'Llave de acceso',
1795
+ 'mfa.add_passkey': 'Añadir una llave de acceso',
1796
+ 'mfa.passkey_needs_js': 'Añadir una llave de acceso requiere JavaScript; una aplicación de autenticación funciona sin él.',
1797
+ 'mfa.passkey_failed': 'La llave de acceso no funcionó. Inténtelo de nuevo o use un código.',
1798
+ 'mfa.passkey_known': 'Esta llave de acceso ya está añadida.',
1799
+ 'mfa.passkey_added': 'Llave de acceso añadida: a partir de ahora puede iniciar sesión con ella.',
1800
+ 'mfa.remove_passkey': 'Quitar esta llave de acceso',
1801
+ 'mfa.use_passkey': 'Usar una llave de acceso',
1802
+ 'mfa.trust_browser': 'Confiar en este navegador durante {days} días',
1803
+ 'mfa.trusted_title': 'Navegadores de confianza',
1804
+ 'mfa.trusted_count': 'Navegadores que omiten el segundo paso: {n} (hasta {days} días).',
1805
+ 'mfa.trusted_off': 'La confianza en navegadores está desactivada.',
1806
+ 'mfa.forget_browsers': 'Olvidar los navegadores de confianza',
1807
+ 'mfa.browsers_forgotten': 'Todos los navegadores vuelven a pedir el segundo paso.',
1808
+ 'mfa.trust_days': 'Confiar en un navegador (días)',
1809
+ 'mfa.trust_days_help': 'Tras el segundo paso, un desarrollador puede omitirlo en ese navegador durante estos días. 0: nunca.',
1810
+ 'mfa.passkey_needs_host': 'Las llaves de acceso necesitan un nombre de host, no una dirección IP ({host}): abra el constructor en localhost o en su nombre de dominio.',
1811
+ 'signin.passkey': 'Iniciar sesión con una llave de acceso',
1812
+ 'mfa.passkey_signin': 'Los desarrolladores pueden iniciar sesión solo con una llave de acceso (sin contraseña)',
1813
+ 'mfa.passkey_signin_help': 'Solo cuando el dispositivo verificó al desarrollador (huella, rostro o PIN): la llave cuenta entonces como ambos factores.',
1814
+ 'mfa.weak_required': 'El inicio de sesión en dos pasos es obligatorio: cambie la contraseña predeterminada antes de continuar.',
1815
+ 'wsp.mfa': 'Inicio de sesión en dos pasos',
1816
+ 'wsp.mfa_required': 'Exigir el inicio de sesión en dos pasos a los desarrolladores de este espacio de trabajo',
1817
+ 'wsp.mfa_required_help': 'Además de la política de la instancia. Los desarrolladores sin llave de acceso ni app de autenticación tienen el periodo de gracia de la instancia y después solo su página de seguridad.',
1818
+ 'wsp.mfa_since': 'Obligatorio desde {date}.',
1819
+ 'wsp.mfa_without': 'Aún sin configurar: {list}.',
1820
+ 'wsp.mfa_save': 'Guardar la regla de dos pasos',
1821
+ 'wsp.mfa_on': 'Los desarrolladores de este espacio de trabajo necesitan el inicio de sesión en dos pasos.',
1822
+ 'wsp.mfa_off': 'El espacio de trabajo ya no exige el inicio de sesión en dos pasos (la política de la instancia sigue vigente).',
1787
1823
  };