@betterportal/auth-default 10.6.21 → 10.6.23

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.
Files changed (128) hide show
  1. package/README.md +140 -10
  2. package/bsb-plugin.json +1 -1
  3. package/lib/account.d.ts +16 -0
  4. package/lib/account.d.ts.map +1 -0
  5. package/lib/account.js +274 -0
  6. package/lib/account.js.map +1 -0
  7. package/lib/accountUI.d.ts +5 -0
  8. package/lib/accountUI.d.ts.map +1 -0
  9. package/lib/accountUI.js +68 -0
  10. package/lib/accountUI.js.map +1 -0
  11. package/lib/bp-contracts/org.betterportal.auth.default.json +2031 -707
  12. package/lib/clientAddress.d.ts +10 -0
  13. package/lib/clientAddress.d.ts.map +1 -0
  14. package/lib/clientAddress.js +35 -0
  15. package/lib/clientAddress.js.map +1 -0
  16. package/lib/config.d.ts +3 -0
  17. package/lib/config.d.ts.map +1 -0
  18. package/lib/config.js +28 -0
  19. package/lib/config.js.map +1 -0
  20. package/lib/defaultAuthManifest.d.ts +7 -0
  21. package/lib/defaultAuthManifest.d.ts.map +1 -1
  22. package/lib/factors.d.ts +18 -0
  23. package/lib/factors.d.ts.map +1 -0
  24. package/lib/factors.js +95 -0
  25. package/lib/factors.js.map +1 -0
  26. package/lib/identity.d.ts +106 -0
  27. package/lib/identity.d.ts.map +1 -0
  28. package/lib/identity.js +315 -0
  29. package/lib/identity.js.map +1 -0
  30. package/lib/index.d.ts +4 -0
  31. package/lib/index.d.ts.map +1 -1
  32. package/lib/index.js +4 -0
  33. package/lib/index.js.map +1 -1
  34. package/lib/mail.d.ts +19 -0
  35. package/lib/mail.d.ts.map +1 -0
  36. package/lib/mail.js +102 -0
  37. package/lib/mail.js.map +1 -0
  38. package/lib/plugins/service-betterportal-auth-default/.bp-generated/registry.d.ts +435 -11
  39. package/lib/plugins/service-betterportal-auth-default/.bp-generated/registry.d.ts.map +1 -1
  40. package/lib/plugins/service-betterportal-auth-default/.bp-generated/registry.js +85 -0
  41. package/lib/plugins/service-betterportal-auth-default/.bp-generated/registry.js.map +1 -1
  42. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/GET.d.ts +34 -0
  43. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/GET.d.ts.map +1 -0
  44. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/GET.js +25 -0
  45. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/GET.js.map +1 -0
  46. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/POST.d.ts +42 -0
  47. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/POST.d.ts.map +1 -0
  48. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/POST.js +27 -0
  49. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/POST.js.map +1 -0
  50. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/_renderer.bootstrap5/GET.d.ts +2 -0
  51. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/_renderer.bootstrap5/GET.d.ts.map +1 -0
  52. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/_renderer.bootstrap5/GET.js +2 -0
  53. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/_renderer.bootstrap5/GET.js.map +1 -0
  54. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/_renderer.bootstrap5/POST.d.ts +2 -0
  55. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/_renderer.bootstrap5/POST.d.ts.map +1 -0
  56. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/_renderer.bootstrap5/POST.js +2 -0
  57. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/_renderer.bootstrap5/POST.js.map +1 -0
  58. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/index.d.ts +3 -0
  59. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/index.d.ts.map +1 -0
  60. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/index.js +3 -0
  61. package/lib/plugins/service-betterportal-auth-default/bp-routes/account/index.js.map +1 -0
  62. package/lib/plugins/service-betterportal-auth-default/bp-routes/login/_renderer.bootstrap5/GET.d.ts.map +1 -1
  63. package/lib/plugins/service-betterportal-auth-default/bp-routes/login/_renderer.bootstrap5/GET.js +11 -8
  64. package/lib/plugins/service-betterportal-auth-default/bp-routes/login/_renderer.bootstrap5/GET.js.map +1 -1
  65. package/lib/plugins/service-betterportal-auth-default/bp-routes/refresh/POST.d.ts +3 -1
  66. package/lib/plugins/service-betterportal-auth-default/bp-routes/refresh/POST.d.ts.map +1 -1
  67. package/lib/plugins/service-betterportal-auth-default/bp-routes/refresh/POST.js +10 -23
  68. package/lib/plugins/service-betterportal-auth-default/bp-routes/refresh/POST.js.map +1 -1
  69. package/lib/plugins/service-betterportal-auth-default/bp-routes/register/_renderer.bootstrap5/GET.d.ts.map +1 -1
  70. package/lib/plugins/service-betterportal-auth-default/bp-routes/register/_renderer.bootstrap5/GET.js +2 -2
  71. package/lib/plugins/service-betterportal-auth-default/bp-routes/register/_renderer.bootstrap5/GET.js.map +1 -1
  72. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/GET.d.ts +34 -0
  73. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/GET.d.ts.map +1 -0
  74. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/GET.js +26 -0
  75. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/GET.js.map +1 -0
  76. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/POST.d.ts +31 -0
  77. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/POST.d.ts.map +1 -0
  78. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/POST.js +23 -0
  79. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/POST.js.map +1 -0
  80. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/_renderer.bootstrap5/GET.d.ts +3 -0
  81. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/_renderer.bootstrap5/GET.d.ts.map +1 -0
  82. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/_renderer.bootstrap5/GET.js +36 -0
  83. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/_renderer.bootstrap5/GET.js.map +1 -0
  84. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/index.d.ts +3 -0
  85. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/index.d.ts.map +1 -0
  86. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/index.js +3 -0
  87. package/lib/plugins/service-betterportal-auth-default/bp-routes/social/index.js.map +1 -0
  88. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/GET.d.ts +32 -0
  89. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/GET.d.ts.map +1 -0
  90. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/GET.js +39 -0
  91. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/GET.js.map +1 -0
  92. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/POST.d.ts +41 -0
  93. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/POST.d.ts.map +1 -0
  94. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/POST.js +103 -0
  95. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/POST.js.map +1 -0
  96. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/_renderer.bootstrap5/GET.d.ts +3 -0
  97. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/_renderer.bootstrap5/GET.d.ts.map +1 -0
  98. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/_renderer.bootstrap5/GET.js +19 -0
  99. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/_renderer.bootstrap5/GET.js.map +1 -0
  100. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/index.d.ts +3 -0
  101. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/index.d.ts.map +1 -0
  102. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/index.js +3 -0
  103. package/lib/plugins/service-betterportal-auth-default/bp-routes/users/index.js.map +1 -0
  104. package/lib/plugins/service-betterportal-auth-default/index.d.ts +41 -4
  105. package/lib/plugins/service-betterportal-auth-default/index.d.ts.map +1 -1
  106. package/lib/plugins/service-betterportal-auth-default/index.js +205 -9
  107. package/lib/plugins/service-betterportal-auth-default/index.js.map +1 -1
  108. package/lib/plugins/service-betterportal-auth-default/loginFlow.d.ts +17 -2
  109. package/lib/plugins/service-betterportal-auth-default/loginFlow.d.ts.map +1 -1
  110. package/lib/plugins/service-betterportal-auth-default/loginFlow.js +83 -62
  111. package/lib/plugins/service-betterportal-auth-default/loginFlow.js.map +1 -1
  112. package/lib/plugins/service-betterportal-auth-default/logoutFlow.js +1 -1
  113. package/lib/plugins/service-betterportal-auth-default/logoutFlow.js.map +1 -1
  114. package/lib/plugins/service-betterportal-auth-default/registrationFlow.d.ts +4 -2
  115. package/lib/plugins/service-betterportal-auth-default/registrationFlow.d.ts.map +1 -1
  116. package/lib/plugins/service-betterportal-auth-default/registrationFlow.js +15 -17
  117. package/lib/plugins/service-betterportal-auth-default/registrationFlow.js.map +1 -1
  118. package/lib/schemas/service-betterportal-auth-default.json +26 -2
  119. package/lib/schemas/service-betterportal-auth-default.plugin.json +27 -3
  120. package/lib/social.d.ts +40 -0
  121. package/lib/social.d.ts.map +1 -0
  122. package/lib/social.js +178 -0
  123. package/lib/social.js.map +1 -0
  124. package/lib/storage.d.ts +72 -0
  125. package/lib/storage.d.ts.map +1 -0
  126. package/lib/storage.js +427 -0
  127. package/lib/storage.js.map +1 -0
  128. package/package.json +13 -6
package/README.md CHANGED
@@ -1,16 +1,146 @@
1
- # BetterPortal Auth Plugin for Node.js
1
+ # BetterPortal Default Auth
2
2
 
3
- Default BetterPortal v10 auth plugin package.
3
+ Default Auth owns the account directory; BP continues to own application permissions and role definitions. User IDs, groups and credentials are scoped to a tenant and, by default, an app. Access tokens and refresh sessions are always app-bound. No BP session is shared across apps.
4
4
 
5
- The file-backed user store is single-process; do not share it between auth replicas. First-admin registration is exclusive within that process. Logout revokes the presented `X-BP-Refresh` session, and password changes or disabling a user invalidate all their refresh sessions. Revocations survive restarts. Already-issued access tokens remain valid until expiry.
5
+ Requires Node.js 24. Existing WorkOS and Authress login, refresh, role-sync and user-management behavior is unchanged. Their only integration change is the shared confirmation-elevation issuer hook.
6
6
 
7
- This package defines the initial JWT-oriented auth contract for:
7
+ ## Deployment modes
8
8
 
9
- - runtime identity
10
- - control-plane identity
11
- - refresh flows
12
- - user-management-adjacent APIs
9
+ | Setting | Simple (default) | Advanced |
10
+ | --- | --- | --- |
11
+ | `mode` | `simple`, including when omitted | `advanced`, explicitly configured |
12
+ | Storage | Atomic JSON, one writer | PostgreSQL, transactional records and tenant locks |
13
+ | Tenants per instance | One, using the BP tenant lock | Multiple activated tenants |
14
+ | Accounts | Ten total, including disabled/unverified/bootstrap accounts | No application account cap |
15
+ | Replicas | One | Multiple, with shared installation configuration and signing/encryption keys |
13
16
 
14
- It depends on the BetterPortal framework contracts and keeps auth as a plugin category rather than core behavior.
17
+ Plugin configuration retains `issuer`, `audience`, `accessTokenSeconds` (900), `refreshTokenSeconds` (604800), `keyStorePath` and `userStorePath`. Advanced adds `postgresUrl` and requires `encryptionKey`: a base64-encoded 32-byte key. Supply secrets using your deployment's secret configuration, and use the same issuer, audience, keys and PostgreSQL database on every replica. Provision the RSA key file before starting Advanced; never let individual replicas independently generate signing identities.
15
18
 
16
- After-login and after-logout destinations are app-owned `app.auth.redirects` view references configured in Tenants & Apps, not provider addon fields.
19
+ Simple creates a separate `<userStorePath>.crypto.json` key file with owner-only permissions. Back up this file with the identity store and signing keys. Advanced uses that exact encryption key when migrating existing encrypted factors or queued mail. JWT signing keys and the factor/mail encryption key have different purposes.
20
+
21
+ The JSON lock refuses concurrent writers. A demonstrably dead process on the same host can be recovered automatically; ambiguous ownership, another host, or an interrupted lock-recovery operation requires the operator to verify the former process has stopped before removing the indicated `.lock` / `.lock.recover` file. Graceful service disposal drains storage and releases its lock.
22
+
23
+ ### One-way migration
24
+
25
+ 1. Stop the Simple instance and back up identity, signing, encryption and BP bootstrap state.
26
+ 2. Set `mode: advanced`, `postgresUrl` and the shared `encryptionKey`. Keep the original `userStorePath` for this startup.
27
+ 3. Start one migration instance. It writes Advanced intent before database work, imports within a transaction, verifies every imported record, records the source checksum in PostgreSQL, marks completion and removes the active JSON identity file.
28
+ 4. Start the remaining replicas with the same installation configuration and keys. Preserve each replica's durable state location.
29
+
30
+ `<userStorePath>.mode.json` and the PostgreSQL migration ledger survive cleanup. Simple startup rejects the marker even if `mode` is omitted or the JSON file is absent. A database outage never opens a JSON fallback. Restarting with an identical leftover source completes cleanup; a different source fails closed for operator investigation. Permission-restricted `.v1.backup` and `.advanced.backup` files are deliberate recovery copies: retain/remove them under your backup policy. Signing keys, encryption keys and BP bootstrap/configuration state are never removed by migration.
31
+
32
+ Legacy accounts keep IDs, bcrypt hashes and existing app-role assignments. Their directory remains tenant-scoped and locked, and existing app assignments—including empty assignments—are marked initialized. Legacy bcrypt comparison preserves its original 72-byte truncation behavior, including longer UTF-8 passwords. Successful logins upgrade passwords up to 1024 bytes to Argon2id using the full submitted password; longer legacy passwords retain bcrypt until changed to a password within the current limit. Refresh sessions are invalidated during import; users sign in again. A legacy Simple store above ten users retains login/recovery but cannot create accounts; a store with accounts in multiple tenants requires Advanced.
33
+
34
+ Legacy identifier collisions never merge accounts or choose an owner by file order. Colliding usernames retain exact, case-sensitive password sign-in; ambiguous normalized identifiers remain reserved and cannot be used for email lookup or new registrations. Conflicting emails are preserved as `legacyEmail` migration metadata and removed from the active email field. These accounts can sign in with their original username and password, subject to MFA, then add and verify a unique email in Account. The original store remains in the migration backup.
35
+
36
+ All migrated legacy accounts retain password login, subject to enabled-account checks and MFA policy, even when mail delivery is not configured. This migration-only exception also applies to accounts with a retained email; migration never marks the address verified. Accounts without an active email can add and verify one from Account. New self-service registrations still require verified email.
37
+
38
+ ## Tenant and app configuration
39
+
40
+ Set `userIsolation` only at tenant scope:
41
+
42
+ - `app`: each app has separate users, identifiers, groups and credentials. This is the default for new tenants.
43
+ - `tenant`: apps share the tenant's account directory. Role assignments, external redirects, sessions and tokens remain app-specific.
44
+
45
+ The setting is persisted and locked by the first account creation, including bootstrap, invitation acceptance and social provisioning. The config API rejects subsequent changes; identity transactions also enforce the boundary. Sharing a tenant directory does not log a user into another app.
46
+
47
+ For tenant-shared accounts, set tenant-only `directoryAdminAppId` to the trusted administration app in that tenant. Only its authorized administrators (and the existing management app) can disable shared accounts, revoke all their sessions, or modify group membership. Other app administrators can assign roles, invite accounts and map existing groups to their own app's roles. They cannot grant themselves directory authority through app configuration. Account listing uses cursor pagination, and role editing keeps direct assignments separate from group-derived permissions.
48
+
49
+ App settings:
50
+
51
+ | Setting | Behavior |
52
+ | --- | --- |
53
+ | `registration` | `invite-only` by default; `public` and `closed` supported |
54
+ | `defaultRoleIds` | JSON array, default `[]`; existing app role IDs only, never `*` |
55
+ | `requireMfa` | Require factor enrollment/verification at login |
56
+ | `mailTransport` | `postal` or `http` |
57
+ | `mailUrl`, `mailFrom` | Delivery endpoint/base URL and sender |
58
+ | `mailApiKey` | Protected Postal server API key |
59
+ | `mailHeaders` | Protected JSON object of headers for a custom HTTP service |
60
+ | `socialConnections` | Protected JSON array of connection objects described below |
61
+
62
+ Default roles are applied once at the first completed login, after required verification/factors. Empty initialization is persisted too. Administrative/invitation assignments take precedence. Changing defaults or revoking roles never regrants them on later login. Group membership contributes only the current app's role mappings. The existing `*` root meaning is unchanged; ordinary users have no roles unless assigned.
63
+
64
+ ## Mount the account routes
65
+
66
+ Alongside existing `/login`, `/logout`, `/register` and `/refresh`, mount these routes in each app that uses them:
67
+
68
+ - `/account` (`account.index`): registration, email verification/resend, password reset, profile/password/email changes, factor enrollment/removal, recovery-code display and active-session revocation.
69
+ - `/social` (`social.index`): social sign-in and explicit account linking; its app-shell URL is the provider's registered callback.
70
+ - `/users` (`users.index`): account enable/disable, invitations, app roles, groups, delivery failures and audit events. Grant its read/update permissions to the intended administrators. Mutations require MFA elevation.
71
+
72
+ The Bootstrap renderer runs in the BP shell and uses its shared auth runtime. Email links keep their one-use proof in the URL fragment and submit it only in a POST body, keeping it out of access-log URLs. They use the mounted app-shell account URL; enabling public registration or inviting users without that mount fails instead of sending an unusable link.
73
+
74
+ `/register` remains deployment bootstrap, restricted to the configured management tenant/app and a deployment setup token. Advanced requires the same explicit `setupToken` on every replica; Simple can generate an owner-only `<userStorePath>.setup.json` file. Customer registration does not close management bootstrap; its own durable completion marker prevents a second administrator bootstrap. The bootstrap administrator must enroll TOTP or a passkey before receiving a usable session. This endpoint cannot grant root in arbitrary apps, and the administrative user API cannot modify the bootstrap identity.
75
+
76
+ Passwords created/reset now use Argon2id, a minimum of 12 characters and a maximum of 1024 UTF-8 bytes, with common-password rejection. Password recovery requires email verification and preserves enrolled factors. TOTP uses replay protection; passkeys require user verification and validate the RP, origin, challenge and credential ownership. Enrollment produces ten one-use recovery codes, displayed once and stored hashed. Enroll a replacement before removing the last factor.
77
+
78
+ For tenant-shared accounts, passkeys only work on their registered hostname. Keep an authenticator or recovery codes available for other app hostnames. If neither is available, sign in on the passkey's original app and add an authenticator in Account before retrying the other app. Login and elevation return recovery instructions instead of an empty challenge; they never bypass existing MFA or authorize replacement enrollment using only a password.
79
+
80
+ Expired and revoked sessions are pruned within their app during session issuance and account-page reads. The account page queries only the signed-in user’s active sessions and shows 100 per page.
81
+
82
+ Login throttling applies both an account budget and a 100-attempt/10-minute client-address budget. Behind a reverse proxy, configure `betterportal.trustedProxyIps` with the immediate proxy addresses and enable `trustedProxyHeaders` for `X-Forwarded-For` (or `cfProxy` for Cloudflare’s `CF-Connecting-IP`). The proxy must overwrite or append the actual connecting client address. Forwarded chains are traversed from the socket peer toward the first untrusted address; spoofed addresses beyond that hop are ignored. Untrusted peers, missing headers and malformed chains fall back to the socket address. With no proxy trust configured, clients behind one proxy share that socket-address budget.
83
+
84
+ The refresh token rotates on every refresh. Replay revokes its family; logout, account disable, password changes and factor changes revoke the applicable sessions. Browser refreshes use the Web Locks API to coordinate tabs where available. Already-issued access tokens at other services remain bounded by their normal expiry; immediate distributed access-token revocation is not promised.
85
+
86
+ ### Email delivery
87
+
88
+ Postal sends JSON to `<mailUrl>/api/v1/send/message`, with `X-Server-API-Key` and Postal's `to`, `from`, `subject`, `plain_body`, `html_body` fields. Both HTTP success and a JSON `status: "success"` are required. See [Postal's send API](https://apiv1.postalserver.io/controllers/send/message.html).
89
+
90
+ Custom HTTP delivery POSTs `{from,to,subject,text,html}` to `mailUrl` using the configured server-side headers; any 2xx response succeeds. HTTPS is required, with a localhost exception for development. Redirects are rejected. There is no SMTP transport.
91
+
92
+ Mail jobs commit with their corresponding identity/challenge mutations and encrypt their sensitive payloads. A leased worker retries failures up to five times with backoff. PostgreSQL selects at most 100 due pending/expired-lease jobs using a partial index, excluding delivery history. Delivery revalidates the current configured URL before decrypting or sending a queued message. Simple limits outstanding/failed jobs to 500 per app and retains the latest 100 successful delivery records. `/users` queries failed deliveries through a partial index and shows 100 per page with cursor navigation and retry, without exposing keys or reset links. Provider response bodies and credentials are not logged. Delivery is at least once: a crash after delivery but before acknowledgement can resend the same one-use link.
93
+
94
+ ### Social connections
95
+
96
+ Each entry contains `id`, `kind` (`google`, `microsoft`, `github`), `clientId`, `clientSecret`, and—for Microsoft—an explicit directory `tenantId`. Register each app's own `/social` shell URL with its provider. Microsoft personal accounts can use their dedicated consumer directory; unrestricted `common`/multi-issuer discovery is intentionally not enabled.
97
+
98
+ Google/Microsoft use OIDC code flow; GitHub uses its OAuth code and profile/email APIs. All use S256 PKCE and short-lived, one-use server state bound to a browser-held secret and the initiating app/origin. OIDC additionally validates nonce, issuer and audience through `openid-client`. Provider access tokens are discarded after identity lookup.
99
+
100
+ New social provisioning requires public registration and a verified provider email. An existing matching email is never automatically linked: sign in first and explicitly link the provider from Account. Providers that do not attest a verified email (including some Microsoft tenants) require email registration/verification first. Linked identities use issuer + stable subject, independent of subsequent email changes.
101
+
102
+ Link approval is bound to the initiating app session and survives the provider redirect and ordinary refresh; another or revoked session cannot complete it. Unlinking requires elevation and a verified local password as a remaining sign-in method. Provider unlinking revokes existing sessions.
103
+
104
+ ## Elevated authentication
105
+
106
+ Declare it alongside existing route authorization:
107
+
108
+ ```ts
109
+ export const auth = {
110
+ required: true,
111
+ permissions: [], // Supply this action's actual permissions.
112
+ elevation: { minimum: "mfa" as const, maxAgeSeconds: 300 }
113
+ };
114
+ ```
115
+
116
+ Use `minimum: "confirm"` for confirmation-only actions. The presence of `elevation` makes authentication required even if `required` was omitted/false. Permissions are checked first; root does not bypass elevation. For a resource-dependent decision, call `ctx.requireElevation({minimum: "mfa"})` after resource authorization and before any side effect. Streaming routes should declare elevation on the route, before streaming begins.
117
+
118
+ Insufficient assurance returns HTTP 401 with RFC 9470's `insufficient_user_authentication`, `WWW-Authenticate`, and a scoped `BP-Auth-Challenge` header. A sufficient elevated token goes directly to the handler without another challenge.
119
+
120
+ The runtime asks the configured app auth service at `/.well-known/bp/auth/elevate`, displays its confirmation/factor flow, then retries the captured request exactly once. It preserves submitted method/body/headers and rejects stale page/session results. Cancellation does not log out. Background loads do not open dialogs, and file uploads require explicit verification/resubmission. Do not perform effects before returning a challenge; network failures after a retried mutation are never automatically retried again.
121
+
122
+ The signed claim is `elevation: {assurance: "confirmed" | "mfa", verifiedAt, expiresAt}`. It covers all eligible actions in that app until expiry, subject to each action's minimum assurance, freshness and ordinary permissions. Confirmation never satisfies MFA. Elevation cannot outlive the access token, is kept only in browser memory, and disappears on normal refresh, logout, reload or session/app changes. No elevated refresh token is issued.
123
+
124
+ Default Auth verifies enrolled factors and current session state. WorkOS/Authress expose the generic confirmation fallback only; it cannot satisfy MFA, revalidate upstream state or extend the original access token lifetime. Custom auth providers opt into the issuer hook and may override `beginAuthElevation` / `finishAuthElevation`. SMS/WhatsApp, SAML, generic enterprise OIDC, SCIM and external API grants remain later adapters; they are not advertised as implemented providers.
125
+
126
+ Generic confirmation uses a purpose-separated signed receipt, bound to the source token, subject and app, with a maximum five-minute completion window. Replicas sharing signing keys can complete it after a redirect or restart. Completion is repeatable within that window, but its original assurance timestamp and access expiry cannot be renewed by replay. Default Auth factor challenges remain stored and one-use.
127
+
128
+ Node, Python and .NET enforce declared elevation before handlers and provide conditional helpers. Go/PHP currently ship protocol helpers only, consistent with their existing SDK scope: hosts must first verify JWT signatures, tenant/app binding and permissions, and must fail closed when they cannot enforce a declared requirement.
129
+
130
+ ## Verification
131
+
132
+ Run the normal workspace build, lint and test gates. Additional checks:
133
+
134
+ ```sh
135
+ # Use an isolated test database, never production.
136
+ BP_AUTH_TEST_POSTGRES=postgres://... node --import tsx services/nodejs/auth-default/tests/postgres.test.ts
137
+ python framework/conformance/check_elevation.py
138
+ cd framework/go && go test ./...
139
+ # From repository root:
140
+ php framework/php/tests/elevation.php
141
+ dotnet framework/dotnet/Conformance/bin/Debug/net10.0/Conformance.dll --elevation framework/conformance/elevation-cases.json
142
+ ```
143
+
144
+ Shared fixtures cover minimum assurance, freshness, expiry, token type and root behavior. Browser tests cover exact mutation replay, cancellation, memory-only elevation and app-specific credential migration. Storage tests exercise caps, isolation, default roles, challenge rollback/replay, TOTP/recovery, legacy migration and PostgreSQL replica races.
145
+
146
+ WebAuthn tests use Chromium's virtual authenticator and verify enrollment, origin, ownership and replay. Social identity tests mock provider responses, and email delivery tests mock the HTTP transport. Live provider sign-ins, production delivery, deployment load and recovery procedures must be verified with the deployment's actual configuration.
package/bsb-plugin.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "id": "service-betterportal-auth-default",
5
5
  "name": "service-betterportal-auth-default",
6
6
  "basePath": "./",
7
- "description": "Default BetterPortal v10 auth service: JWKS, login/logout/refresh, bcrypt user store",
7
+ "description": "Default BetterPortal auth: app-scoped accounts, social sign-in, MFA and JWT sessions",
8
8
  "tags": [
9
9
  "betterportal",
10
10
  "auth",
@@ -0,0 +1,16 @@
1
+ import { type RouteHandlerContext, type JsonObject } from "@betterportal/framework";
2
+ import type { AuthRuntime } from "./plugins/service-betterportal-auth-default/index.js";
3
+ type Context = RouteHandlerContext & {
4
+ plugin: {
5
+ runtime: AuthRuntime;
6
+ };
7
+ };
8
+ export declare function publishSession(ctx: Context, issued: Awaited<ReturnType<AuthRuntime["identity"]["issueSession"]>>): void;
9
+ export declare function accountLink(ctx: Context, action: string, ticket: {
10
+ id: string;
11
+ secret: string;
12
+ }): string | null;
13
+ export declare function accountGet(ctx: Context): Promise<JsonObject>;
14
+ export declare function accountPost(ctx: Context): Promise<JsonObject>;
15
+ export {};
16
+ //# sourceMappingURL=account.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account.d.ts","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"AAAA,OAAO,EAA4C,KAAK,mBAAmB,EAAE,KAAK,UAAU,EAAE,MAAM,yBAAyB,CAAC;AAE9H,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sDAAsD,CAAC;AAGxF,KAAK,OAAO,GAAG,mBAAmB,GAAG;IAAE,MAAM,EAAE;QAAE,OAAO,EAAE,WAAW,CAAA;KAAE,CAAA;CAAE,CAAC;AAC1E,wBAAgB,cAAc,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,OAAO,CAAC,UAAU,CAAC,WAAW,CAAC,UAAU,CAAC,CAAC,cAAc,CAAC,CAAC,CAAC,GAAG,IAAI,CAIvH;AACD,wBAAgB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE;IAAE,EAAE,EAAE,MAAM,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,GAAG,IAAI,CAG/G;AAED,wBAAsB,UAAU,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAmBlE;AACD,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,GAAG,OAAO,CAAC,UAAU,CAAC,CAwKnE"}
package/lib/account.js ADDED
@@ -0,0 +1,274 @@
1
+ import { requireElevation, resolveAppAuthRedirect } from "@betterportal/framework";
2
+ import { AuthError, hashPassword, verifyPassword, isValidEmail } from "./identity.js";
3
+ export function publishSession(ctx, issued) {
4
+ ctx.bpHeaders?.set("Authorization", `Bearer ${issued.accessToken}`, { expiresInSeconds: issued.accessTokenExpiresInSeconds, locked: true, refreshPath: "/refresh", refreshBeforeSeconds: 60 });
5
+ ctx.bpHeaders?.set("X-BP-Refresh", issued.refreshToken, { expiresInSeconds: issued.refreshTokenExpiresInSeconds, locked: true, scopeToOwner: true });
6
+ if (ctx.serviceId)
7
+ ctx.responseHeaders?.set("HX-Trigger", `bp:fragments:${ctx.serviceId}`);
8
+ }
9
+ export function accountLink(ctx, action, ticket) {
10
+ const url = ctx.uiRouteUrl?.("account.index", { absolute: true });
11
+ return url ? `${url}#bp-auth=${encodeURIComponent(JSON.stringify({ action, ...ticket }))}` : null;
12
+ }
13
+ const publicUser = (u) => ({ id: u.id, username: u.username, email: u.email ?? "", name: u.name ?? "", enabled: u.enabled, emailVerified: u.emailVerified, totp: !!u.totp });
14
+ export async function accountGet(ctx) {
15
+ const runtime = ctx.plugin.runtime;
16
+ const scope = { tenantId: ctx.tenant.id, appId: ctx.app.id };
17
+ const policy = await runtime.policy(scope);
18
+ ctx.responseHeaders?.set("Cache-Control", "no-store");
19
+ ctx.responseHeaders?.set("Referrer-Policy", "no-referrer");
20
+ const accountUrl = ctx.routeUrl?.("account.index", { absolute: true }) ?? "/account";
21
+ if (!ctx.user)
22
+ return { status: "ok", accountUrl, registration: policy.registration, signedIn: false };
23
+ const user = await runtime.identity.findUser(scope, policy, ctx.user.sub, true);
24
+ if (!user?.enabled)
25
+ throw new AuthError("Sign in again.", 401);
26
+ const sessionPage = await runtime.identity.storage.transaction(scope, async (tx) => {
27
+ const now = Date.now();
28
+ await tx.pruneSessions(scope, now);
29
+ return tx.activeSessions(scope, user.id, now, { after: typeof ctx.query?.sessionAfter === "string" ? ctx.query.sessionAfter : undefined, limit: 101 });
30
+ });
31
+ const sessions = sessionPage.slice(0, 100).map(s => ({ id: s.id, createdAt: s.createdAt, expiresAt: s.expiresAt }));
32
+ const sessionNextUrl = sessionPage.length > 100 ? ctx.routeUrl?.("account.index", { absolute: true, query: { sessionAfter: sessions.at(-1).id } }) ?? "" : "";
33
+ const passkeys = await runtime.identity.storage.transaction(scope, async (tx) => (await tx.list("passkey", user)).filter(p => p.userId === user.id).map(p => ({ id: p.id, name: String(p.name), rpId: String(p.rpId) })));
34
+ const linkedIdentities = await runtime.identity.storage.transaction(scope, async (tx) => (await tx.list("external", user)).filter(p => p.userId === user.id).map(p => ({ id: p.id, provider: String(p.provider) })));
35
+ return { status: "ok", passkeys, linkedIdentities, accountUrl, socialUrl: ctx.routeUrl?.("social.index", { absolute: true }) ?? "/social", registration: policy.registration, signedIn: true, user: publicUser(user), sessions, sessionNextUrl };
36
+ }
37
+ export async function accountPost(ctx) {
38
+ const runtime = ctx.plugin.runtime;
39
+ const identity = runtime.identity;
40
+ const scope = { tenantId: ctx.tenant.id, appId: ctx.app.id };
41
+ const policy = await runtime.policy(scope);
42
+ const body = ctx.request;
43
+ const action = String(body.action);
44
+ const dir = { tenantId: scope.tenantId, appId: policy.isolation === "app" ? scope.appId : "" };
45
+ const event = ctx.rawEvent;
46
+ const origin = event.req.headers.get("origin") ?? event.url.origin;
47
+ const accountUrl = ctx.routeUrl?.("account.index", { absolute: true }) ?? "/account";
48
+ ctx.responseHeaders?.set("Cache-Control", "no-store");
49
+ ctx.responseHeaders?.set("Referrer-Policy", "no-referrer");
50
+ const sendLink = async (user, purpose) => identity.storage.transaction(scope, async (tx) => {
51
+ const ticket = await identity.challengeInTransaction(tx, scope, purpose, { version: user.refreshVersion }, user.id, 1800);
52
+ const link = accountLink(ctx, purpose, ticket);
53
+ if (!link)
54
+ throw new AuthError("Mount the account page in this app before enabling email delivery.", 503);
55
+ await runtime.mail.enqueue(tx, scope, user.email, "BetterPortal account verification", `Open this link to ${purpose === "reset" ? "reset your password" : "verify your email"}: ${link}\nThis link expires in 30 minutes. If you did not request it, ignore this email.`);
56
+ });
57
+ if (["signup", "request-reset", "resend"].includes(action)) {
58
+ const email = String(body.email ?? "").trim().toLowerCase();
59
+ await identity.rateLimit(scope, action, email, 5, 900);
60
+ await identity.rateLimit(scope, "mail-volume", "app", 100, 900);
61
+ if (action === "signup") {
62
+ if (policy.registration !== "public")
63
+ throw new AuthError("Public registration is closed.", 403);
64
+ if (!email)
65
+ throw new AuthError("Email required.");
66
+ // The user and email job commit together; a delivery configuration failure leaves no stranded account.
67
+ const passwordHash = await hashPassword(String(body.password ?? ""));
68
+ try {
69
+ await identity.storage.transaction(scope, async (tx) => {
70
+ const created = await identity.createInTransaction(tx, scope, policy, { username: email, email, name: body.name }, passwordHash);
71
+ const ticket = await identity.challengeInTransaction(tx, scope, "verify", { version: created.refreshVersion }, created.id, 1800);
72
+ const link = accountLink(ctx, "verify", ticket);
73
+ if (!link)
74
+ throw new AuthError("Mount the account page in this app before enabling registration.", 503);
75
+ await runtime.mail.enqueue(tx, scope, email, "Verify your email", `Verify your email address: ${link}\nThis link expires in 30 minutes.`);
76
+ });
77
+ }
78
+ catch (error) {
79
+ if (!(error instanceof AuthError && error.status === 409 && error.message === "This account cannot be registered."))
80
+ throw error;
81
+ }
82
+ }
83
+ else {
84
+ const user = await identity.findUser(scope, policy, email);
85
+ if (user?.enabled && user.email && (action !== "resend" || !user.emailVerified))
86
+ await sendLink(user, action === "resend" ? "verify" : "reset");
87
+ }
88
+ return { status: "ok", message: "If the account is eligible, an email will arrive shortly.", accountUrl };
89
+ }
90
+ if (action === "verify" || action === "reset" || action === "email.verify") {
91
+ const passwordHash = action === "reset" ? await hashPassword(String(body.password ?? "")) : undefined;
92
+ await identity.consume(scope, action, String(body.id), String(body.secret), async (challenge, tx) => {
93
+ const user = await tx.get("user", challenge.userId ?? "", dir);
94
+ if (!user?.enabled || challenge.data.version !== user.refreshVersion)
95
+ throw new AuthError("Verification unavailable.");
96
+ if (action === "reset") {
97
+ user.passwordHash = passwordHash;
98
+ user.emailVerified = true;
99
+ user.refreshVersion++;
100
+ if (user.email)
101
+ await runtime.mail.enqueue(tx, scope, user.email, "Password reset completed", "Your password was reset and existing sessions were revoked. If this was not you, contact your administrator immediately.");
102
+ }
103
+ else if (action === "email.verify") {
104
+ const email = String(challenge.data.email);
105
+ if (await tx.get("identifier", email, dir))
106
+ throw new AuthError("Email unavailable.");
107
+ if (user.email) {
108
+ await tx.remove("identifier", user.email, dir);
109
+ if (user.username === user.email)
110
+ user.username = email;
111
+ }
112
+ user.email = email;
113
+ user.emailVerified = true;
114
+ user.refreshVersion++;
115
+ await tx.put("identifier", dir, { id: email, userId: user.id });
116
+ }
117
+ else
118
+ user.emailVerified = true;
119
+ await tx.put("user", dir, user);
120
+ await identity.audit(tx, scope, user.id, `account.${action}`);
121
+ });
122
+ return { status: "ok", message: action === "reset" ? "Password reset. Sign in with your new password and authentication factor." : "Email verified. You can sign in.", accountUrl };
123
+ }
124
+ if (action === "invite.accept") {
125
+ if (policy.registration === "closed")
126
+ throw new AuthError("Registration is closed.", 403);
127
+ const passwordHash = body.password ? await hashPassword(String(body.password)) : undefined;
128
+ await identity.consume(scope, "invite", String(body.id), String(body.secret), async (challenge, tx) => {
129
+ const email = String(challenge.data.email);
130
+ const roles = challenge.data.roles;
131
+ if (roles.some(id => id === "*" || !policy.allowedRoleIds.includes(id)))
132
+ throw new AuthError("Invitation roles are no longer available.");
133
+ const index = await tx.get("identifier", email, dir);
134
+ if (index) {
135
+ const user = await tx.get("user", String(index.userId), dir);
136
+ if (!ctx.user || ctx.user.sub !== user?.id || !user.enabled || user.email !== email)
137
+ throw new AuthError("Sign in to the existing account before accepting this invitation.", 409);
138
+ await identity.assertActiveSession(tx, scope, ctx.user, user);
139
+ const existing = await tx.get("roles", user.id, scope);
140
+ await tx.put("roles", scope, { id: user.id, roles: [...new Set([...(existing?.roles ?? []), ...roles])], initialized: true });
141
+ await identity.audit(tx, scope, user.id, "invitation.accepted");
142
+ return;
143
+ }
144
+ if (!passwordHash)
145
+ throw new AuthError("Choose a password for your new account.");
146
+ await identity.createInTransaction(tx, scope, policy, { username: email, email, name: body.name, verified: true }, passwordHash, roles);
147
+ });
148
+ return { status: "ok", message: "Invitation accepted. Sign in to continue.", accountUrl };
149
+ }
150
+ if (action === "login.complete") {
151
+ const result = await identity.consume(scope, "login", String(body.id), String(body.secret), async (challenge, tx) => {
152
+ const user = await tx.get("user", challenge.userId ?? "", dir);
153
+ if (!user?.enabled || user.refreshVersion !== challenge.data.version)
154
+ throw new AuthError("Login expired. Start again.");
155
+ const factor = challenge.data.factor;
156
+ if (factor.origin !== origin)
157
+ throw new AuthError("Login belongs to a different origin.");
158
+ const recoveryCodes = await runtime.factors.complete(tx, scope, user, factor, body);
159
+ return { user, recoveryCodes };
160
+ });
161
+ const issued = await identity.issueSession(scope, policy, result.user, runtime.tokenIssuer, runtime.refreshTokenSeconds);
162
+ publishSession(ctx, issued);
163
+ return { status: "ok", message: "Signed in.", next: resolveAppAuthRedirect(ctx, "afterLogin", body.next), recoveryCodes: result.recoveryCodes ?? [], accountUrl };
164
+ }
165
+ if (!ctx.user)
166
+ throw new AuthError("Sign in to manage your account.", 401);
167
+ const user = await identity.findUser(scope, policy, ctx.user.sub, true);
168
+ if (!user?.enabled)
169
+ throw new AuthError("Account unavailable.", 401);
170
+ const assertCurrent = async (tx) => {
171
+ const current = await tx.get("user", user.id, dir);
172
+ if (!current?.enabled)
173
+ throw new AuthError("Account unavailable.", 401);
174
+ await identity.assertActiveSession(tx, scope, ctx.user, current);
175
+ return current;
176
+ };
177
+ await identity.storage.transaction(scope, assertCurrent);
178
+ if (action === "profile") {
179
+ await identity.updateUser(scope, policy, user.id, async (u, tx) => { await assertCurrent(tx); u.name = String(body.name ?? "").slice(0, 200); });
180
+ }
181
+ else if (action === "session.revoke") {
182
+ await identity.storage.transaction(scope, async (tx) => {
183
+ await assertCurrent(tx);
184
+ const session = await tx.get("session", String(body.id), scope);
185
+ if (session?.userId !== user.id)
186
+ throw new AuthError("Session unavailable.", 404);
187
+ await tx.put("session", scope, { ...session, revoked: true });
188
+ });
189
+ }
190
+ else if (action === "password") {
191
+ if (!await verifyPassword(user.passwordHash, String(body.currentPassword ?? "")))
192
+ throw new AuthError("Current password invalid.", 403);
193
+ if (await runtime.factors.hasFactors(user))
194
+ requireElevation(ctx.user, { minimum: "mfa", maxAgeSeconds: 300 });
195
+ const hash = await hashPassword(String(body.password));
196
+ await identity.updateUser(scope, policy, user.id, async (u, tx) => { await assertCurrent(tx); if (u.passwordHash !== user.passwordHash)
197
+ throw new AuthError("Account changed."); u.passwordHash = hash; u.refreshVersion++; });
198
+ }
199
+ else if (action === "email.change") {
200
+ requireElevation(ctx.user, { minimum: await runtime.factors.hasFactors(user) ? "mfa" : "confirm", maxAgeSeconds: 300 });
201
+ const email = String(body.email ?? "").trim().toLowerCase();
202
+ if (!isValidEmail(email))
203
+ throw new AuthError("Enter a valid email.");
204
+ await identity.storage.transaction(scope, async (tx) => {
205
+ await assertCurrent(tx);
206
+ const ticket = await identity.challengeInTransaction(tx, scope, "email.verify", { email, version: user.refreshVersion }, user.id, 1800);
207
+ const link = accountLink(ctx, "email.verify", ticket);
208
+ if (!link)
209
+ throw new AuthError("Account page is not mounted.", 503);
210
+ await runtime.mail.enqueue(tx, scope, email, "Verify your new email address", `Confirm your new email address: ${link}`);
211
+ if (user.email)
212
+ await runtime.mail.enqueue(tx, scope, user.email, "Email change requested", "A change to your email address was requested. If this was not you, reset your password and contact your administrator.");
213
+ });
214
+ }
215
+ else if (action === "factor.start") {
216
+ if (await runtime.factors.hasFactors(user))
217
+ requireElevation(ctx.user, { minimum: "mfa", maxAgeSeconds: 300 });
218
+ else
219
+ requireElevation(ctx.user, { minimum: "confirm", maxAgeSeconds: 300 });
220
+ const prepared = await runtime.factors.prepare(user, origin, true);
221
+ const ticket = await identity.challenge(scope, "factor", { version: user.refreshVersion, factor: prepared.private }, user.id);
222
+ return { status: "ok", challenge: { ...ticket, ...prepared.public }, accountUrl };
223
+ }
224
+ else if (action === "factor.complete") {
225
+ const recoveryCodes = await identity.consume(scope, "factor", String(body.id), String(body.secret), async (challenge, tx) => {
226
+ const current = await assertCurrent(tx);
227
+ const factor = challenge.data.factor;
228
+ if (!current?.enabled || challenge.userId !== user.id || current.refreshVersion !== challenge.data.version || factor.origin !== origin)
229
+ throw new AuthError("Account changed.");
230
+ return runtime.factors.complete(tx, scope, current, factor, body);
231
+ });
232
+ return { status: "ok", message: "Factor enrolled. Save your recovery codes now.", recoveryCodes: recoveryCodes ?? [], accountUrl };
233
+ }
234
+ else if (action === "factor.remove") {
235
+ requireElevation(ctx.user, { minimum: "mfa", maxAgeSeconds: 300 });
236
+ await identity.updateUser(scope, policy, user.id, async (u, tx) => {
237
+ await assertCurrent(tx);
238
+ const keys = (await tx.list("passkey", dir)).filter(p => p.userId === user.id);
239
+ if (keys.length + (u.totp ? 1 : 0) <= 1)
240
+ throw new AuthError("Enroll a replacement before removing your last factor.");
241
+ if (body.method === "totp") {
242
+ delete u.totp;
243
+ delete u.totpLastStep;
244
+ }
245
+ else {
246
+ const key = keys.find(k => k.id === body.id);
247
+ if (!key)
248
+ throw new AuthError("Factor unavailable.");
249
+ await tx.remove("passkey", key.id, dir);
250
+ }
251
+ u.refreshVersion++;
252
+ });
253
+ }
254
+ else if (action === "identity.unlink") {
255
+ requireElevation(ctx.user, { minimum: await runtime.factors.hasFactors(user) ? "mfa" : "confirm", maxAgeSeconds: 300 });
256
+ await identity.storage.transaction(scope, async (tx) => {
257
+ const current = await assertCurrent(tx);
258
+ const links = (await tx.list("external", dir)).filter(p => p.userId === current.id);
259
+ if (!links.some(p => p.id === body.id))
260
+ throw new AuthError("Linked identity unavailable.", 404);
261
+ // A verified local password is a recovery-independent sign-in method.
262
+ if (!current.passwordHash || !current.emailVerified)
263
+ throw new AuthError("Set a local password and verify your email before unlinking a provider.");
264
+ await tx.remove("external", String(body.id), dir);
265
+ current.refreshVersion++;
266
+ await tx.put("user", dir, current);
267
+ });
268
+ }
269
+ else
270
+ throw new AuthError("Unknown account action.");
271
+ await identity.storage.transaction(scope, tx => identity.audit(tx, scope, user.id, `account.${action}`));
272
+ return { status: "ok", message: "Account updated.", accountUrl };
273
+ }
274
+ //# sourceMappingURL=account.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"account.js","sourceRoot":"","sources":["../src/account.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,5 @@
1
+ import type { HtmlRenderable } from "@betterportal/framework";
2
+ export declare function accountScript(): HtmlRenderable;
3
+ export declare function renderChallenge(challenge: unknown, endpoint: string, action?: string): HtmlRenderable;
4
+ export declare function renderAccount(data: Record<string, any>): HtmlRenderable;
5
+ //# sourceMappingURL=accountUI.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"accountUI.d.ts","sourceRoot":"","sources":["../src/accountUI.tsx"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,yBAAyB,CAAC;AAE9D,wBAAgB,aAAa,IAAI,cAAc,CAoD9C;AAGD,wBAAgB,eAAe,CAAC,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,EAAE,MAAM,SAAmB,GAAG,cAAc,CAE/G;AACD,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,GAAG,cAAc,CA0BvE"}
@@ -0,0 +1,68 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "jsx-htmx/jsx-runtime";
2
+ /** @jsxImportSource jsx-htmx */
3
+ import { js } from "jsx-htmx";
4
+ export function accountScript() {
5
+ return js(`(() => {
6
+ const root = document.getElementById("bp-account-ui");
7
+ if (!root) return;
8
+ if (location.hash.startsWith("#bp-auth=")) {
9
+ try {
10
+ const ticket = JSON.parse(decodeURIComponent(location.hash.slice(9)));
11
+ if (!["verify", "reset", "email.verify", "invite.accept"].includes(ticket.action) || typeof ticket.id !== "string" || typeof ticket.secret !== "string") throw new Error("Invalid verification link");
12
+ const form = root.querySelector("#bp-link-verification");
13
+ if (form) {
14
+ root.querySelectorAll("form").forEach(other => other.hidden = true);
15
+ form.hidden = false;
16
+ for (const name of ["action", "id", "secret"]) form.elements.namedItem(name).value = ticket[name];
17
+ const password = form.elements.namedItem("password");
18
+ password.required = ticket.action === "reset" || (ticket.action === "invite.accept" && root.dataset.signedIn !== "true");
19
+ password.closest("label").hidden = !password.required; password.disabled = !password.required;
20
+ }
21
+ history.replaceState(null, "", location.pathname + location.search);
22
+ } catch { /* Show the ordinary account recovery controls for an invalid link. */ }
23
+ }
24
+ const output = root.querySelector('[role="status"]');
25
+ const api = window.BetterPortalAuth;
26
+ const complete = async (body) => {
27
+ if (!api) throw new Error("Open this page inside your application.");
28
+ const response = await api.fetch(root.dataset.endpoint, { method: "POST", headers: { "Content-Type": "application/json", Accept: "application/json" }, body: JSON.stringify(body) });
29
+ const result = await response.json();
30
+ if (!response.ok) throw new Error(result.message || "Request failed.");
31
+ if (result.challenge) return factor(result.challenge, "factor.complete");
32
+ output.textContent = result.message || "Saved.";
33
+ if (result.recoveryCodes?.length) { const pre = document.createElement("pre"); pre.textContent = "Save these recovery codes securely. They are shown only once.\\n\\n" + result.recoveryCodes.join("\\n"); output.append(pre); }
34
+ if (result.next) { const link = document.createElement("a"); link.href = result.next; link.textContent = "Continue"; link.className = "btn btn-primary"; output.append(link); }
35
+ };
36
+ const factor = async (challenge, action) => {
37
+ if (!api) throw new Error("Open this page inside your application.");
38
+ const result = await api.factor(challenge);
39
+ if (!result) return;
40
+ await complete({ action, id: challenge.id, secret: challenge.secret, next: new URLSearchParams(location.search).get("next") || "", ...result });
41
+ };
42
+ root.querySelectorAll("form").forEach(form => form.addEventListener("submit", async event => {
43
+ event.preventDefault(); event.stopPropagation(); const button = form.querySelector('button[type="submit"]'); if (button) button.disabled = true;
44
+ try { await complete(Object.fromEntries(new FormData(form))); }
45
+ catch (error) { output.textContent = error.message; }
46
+ finally { if (button) button.disabled = false; }
47
+ }));
48
+ const challengeButton = root.querySelector("[data-challenge]");
49
+ if (challengeButton) challengeButton.addEventListener("click", async () => {
50
+ challengeButton.disabled = true;
51
+ try { await factor(JSON.parse(challengeButton.dataset.challenge), challengeButton.dataset.action); }
52
+ catch (error) { output.textContent = error.message; }
53
+ finally { challengeButton.disabled = false; }
54
+ });
55
+ })()`);
56
+ }
57
+ const field = (name, title, type = "text") => _jsxs("label", { class: "form-label d-block", children: [title, _jsx("input", { class: "form-control", name: name, type: type, required: true, autocomplete: type === "password" ? "new-password" : name })] });
58
+ const form = (action, title, children) => _jsxs("form", { class: "border rounded p-3 mb-3", "data-bp-no-route": "", children: [_jsx("h4", { children: title }), _jsx("input", { type: "hidden", name: "action", value: action }), children, _jsx("button", { class: "btn btn-primary mt-2", type: "submit", children: title })] });
59
+ export function renderChallenge(challenge, endpoint, action = "login.complete") {
60
+ return _jsxs("section", { id: "bp-account-ui", class: "container py-4", style: "max-width:540px", "data-endpoint": endpoint, children: [_jsx("h2", { children: "Verify your identity" }), _jsx("p", { children: "Complete authentication to continue." }), _jsx("button", { class: "btn btn-primary", "data-challenge": JSON.stringify(challenge), "data-action": action, children: "Continue verification" }), _jsx("div", { role: "status", class: "mt-3" }), _jsx("script", { children: accountScript() })] });
61
+ }
62
+ export function renderAccount(data) {
63
+ if (data.challenge)
64
+ return renderChallenge(data.challenge, data.accountUrl ?? "/account", "factor.complete");
65
+ const action = ["verify", "reset", "email.verify", "invite.accept"].includes(data.action) ? data.action : "";
66
+ return _jsxs("section", { id: "bp-account-ui", class: "container py-4", style: "max-width:620px", "data-endpoint": data.accountUrl ?? "/account", "data-signed-in": String(!!data.signedIn), children: [_jsx("h2", { children: "Account" }), _jsxs("form", { id: "bp-link-verification", hidden: true, "data-bp-no-route": "", class: "border rounded p-3 mb-3", children: [_jsx("h4", { children: "Complete account verification" }), _jsx("input", { type: "hidden", name: "action" }), _jsx("input", { type: "hidden", name: "id" }), _jsx("input", { type: "hidden", name: "secret" }), field("password", "New password (at least 12 characters)", "password"), _jsx("button", { class: "btn btn-primary", type: "submit", children: "Continue" })] }), _jsx("div", { role: "status", class: "my-3", "aria-live": "polite", children: data.message ?? "" }), action ? form(action, action === "reset" ? "Reset password" : action === "invite.accept" ? "Accept invitation" : "Verify email", _jsxs(_Fragment, { children: [_jsx("input", { type: "hidden", name: "id", value: data.id }), _jsx("input", { type: "hidden", name: "secret", value: data.secret }), (action === "reset" || (action === "invite.accept" && !data.signedIn)) ? field("password", "New password (at least 12 characters)", "password") : null] })) : data.signedIn ? _jsxs(_Fragment, { children: [_jsx("p", { children: data.user?.email || data.user?.username }), _jsx("a", { href: data.socialUrl, "hx-get": data.socialUrl, "hx-target": "#bp-main", children: "Link Google, Microsoft or GitHub" }), (data.linkedIdentities ?? []).map((link) => form("identity.unlink", "Unlink provider", _jsxs(_Fragment, { children: [_jsx("p", { children: link.provider }), _jsx("input", { type: "hidden", name: "id", value: link.id })] }))), form("profile", "Update name", field("name", "Display name")), form("password", "Change password", _jsxs(_Fragment, { children: [field("currentPassword", "Current password", "password"), field("password", "New password (at least 12 characters)", "password")] })), form("email.change", "Change email", field("email", "New email", "email")), form("factor.start", "Add an authentication factor"), data.user?.totp ? form("factor.remove", "Remove authenticator", _jsx("input", { type: "hidden", name: "method", value: "totp" })) : null, (data.passkeys ?? []).map((key) => form("factor.remove", "Remove passkey", _jsxs(_Fragment, { children: [_jsxs("p", { children: [key.name, " \u00B7 ", key.rpId] }), _jsx("input", { type: "hidden", name: "method", value: "passkey" }), _jsx("input", { type: "hidden", name: "id", value: key.id })] }))), data.sessionNextUrl ? _jsx("a", { href: data.sessionNextUrl, "hx-get": data.sessionNextUrl, "hx-target": "#bp-main", children: "More active sessions" }) : null, _jsx("h3", { children: "Active sessions in this app" }), (data.sessions ?? []).map((session) => form("session.revoke", "Revoke session", _jsxs(_Fragment, { children: [_jsx("p", { children: new Date(session.createdAt).toISOString() }), _jsx("input", { type: "hidden", name: "id", value: session.id })] })))] }) : _jsxs(_Fragment, { children: [data.registration === "public" ? form("signup", "Create account", _jsxs(_Fragment, { children: [field("email", "Email", "email"), field("name", "Display name"), field("password", "Password (at least 12 characters)", "password")] })) : null, form("request-reset", "Send password reset", field("email", "Email", "email")), form("resend", "Resend verification email", field("email", "Email", "email"))] }), _jsx("script", { children: accountScript() })] });
67
+ }
68
+ //# sourceMappingURL=accountUI.js.map