@sanoma-ai/connector-better-auth 0.1.5 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # @sanoma-ai/connector-better-auth
2
2
 
3
- [Better Auth](https://www.better-auth.com) as a visitor login for a [Sanoma](https://github.com/Sanoma-AI/sanoma) public site: visitors sign up and sign in with an email and a password, and their accounts live on the company's own database. It has no operations and no credential; it attaches a login half (`withSetup({ login })`, see [`@sanoma-ai/workflows`](https://www.npmjs.com/package/@sanoma-ai/workflows#usage)), which the app calls once for each public site that names it.
3
+ [Better Auth](https://www.better-auth.com) as a visitor login for a [Sanoma](https://github.com/Sanoma-AI/sanoma) public site, and as the app's staff sign-in on a host: visitors sign up and sign in with an email and a password (or an email code, or GitHub), staff by an email code, an invite link or GitHub, and the accounts live on the company's own database. It has no operations and no credential; it attaches a login half (`withSetup({ login })`, see [`@sanoma-ai/workflows`](https://www.npmjs.com/package/@sanoma-ai/workflows#usage)), which the app calls once for each public site that names it.
4
4
 
5
5
  ## Usage
6
6
 
@@ -31,7 +31,7 @@ permit (principal in Sanoma::Group::"visitors", action == Sanoma::Action::"workf
31
31
 
32
32
  ### No secret to set
33
33
 
34
- It has no credential. Better Auth signs its cookies with a key the app derives for this login from the deployment's `SANOMA_SECRETS_KEY` (HKDF-SHA256, purpose `login better-auth`), which `sanoma` already sets up; changing that key signs every visitor out.
34
+ It has no credential. Better Auth signs its cookies with a key the app derives for this login from the deployment's `SANOMA_SIGNING_KEY` (HKDF-SHA256, purpose `login better-auth`), which `sanoma` already sets up; changing that key signs every visitor out.
35
35
 
36
36
  ### The schema
37
37
 
@@ -41,11 +41,13 @@ The tables (`user`, `session`, `account`, `verification`) are in the schema `bet
41
41
 
42
42
  The login answers under `/auth/better-auth/` on the site's own host, and nothing else. A site's pages are its sign-in forms, drawn from the kit (`sanoma/ui`), that `fetch` JSON with a POST (the browser adds `Origin`, which Better Auth checks):
43
43
 
44
- | Route | Body | What it does |
45
- | -------------------------------------- | ----------------------------- | ------------------------------------------------------------------------ |
46
- | `POST /auth/better-auth/sign-up/email` | `{ name, email, password }` | Makes the account and signs the visitor in (8 characters or more). |
47
- | `POST /auth/better-auth/sign-in/email` | `{ email, password }` | Signs in; a wrong email or password is 401. |
48
- | `POST /auth/better-auth/sign-out` | `{}`, or none (the app's own) | Ends the session; it is also `signOut`, which `useActor()` gives a page. |
44
+ | Route | Body | What it does |
45
+ | -------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------------------------------- |
46
+ | `POST /auth/better-auth/sign-up/email` | `{ name, email, password }` | Makes the account and signs the visitor in (8 characters or more). |
47
+ | `POST /auth/better-auth/sign-in/email` | `{ email, password }` | Signs in; a wrong email or password is 401. |
48
+ | `POST /auth/better-auth/sign-out` | `{}`, or none (the app's own) | Ends the session; it is also `signOut`, which `useActor()` gives a page. |
49
+ | `POST /auth/better-auth/email-otp/send-verification-otp` | `{ email, type: "sign-in" }` | With a sender only: mails a six-digit code, good for five minutes and three tries. |
50
+ | `POST /auth/better-auth/sign-in/email-otp` | `{ email, otp }` | With a sender only: signs in by the code, making the account on first use. |
49
51
 
50
52
  ```ts
51
53
  const response = await fetch("/auth/better-auth/sign-in/email", {
@@ -58,8 +60,44 @@ if (response.ok) window.location.reload(); // the app reads the new session cook
58
60
 
59
61
  The session is one cookie, `better-auth.session_token` (`__Secure-better-auth.session_token`, and `Secure`, when the site is served over https): HttpOnly, SameSite=Lax, for the site's host only (no `Domain`), for seven days from sign-in. A request for a host that is not the site's is answered 421. Sign-in and sign-up are rate limited per visitor address (three tries in ten seconds), which the app gives the login in its own header, never from `X-Forwarded-For`. A site never imports `better-auth`: its pages only post to these routes.
60
62
 
63
+ ### An email code
64
+
65
+ When the company's config names a sender (`login: { sender: resend.emails.send }`), the login also signs visitors in by a code mailed to them, sent as a run of the runtime's `login-code` workflow (under the policy and in the ledger). A visitor signed in by code has a verified email, which the app gives the principal (`ctx.startedBy.email` in a run they start); one signed up by password has none until they verify it.
66
+
67
+ ### GitHub
68
+
69
+ When the app has the company's GitHub App client (`SANOMA_GITHUB_CLIENT_ID` and `SANOMA_GITHUB_CLIENT_SECRET`, which `sanoma` reads on a host, the staff login's too), the login offers GitHub (`methods` lists `"github"`): a visitor signed in links their GitHub account, and is then said to be that GitHub login (`ctx.startedBy.github` in a run they start, `user.github` in the session). Add `https://<site host>/auth/better-auth/callback/github` to the App's callback URLs (its settings, "Callback URL"), one for each site's host.
70
+
71
+ | Route | Body | What it does |
72
+ | --------------------------------------- | ------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
73
+ | `POST /auth/better-auth/link-social` | `{ provider: "github", callbackURL }` | Signed in: answers `{ url, redirect: true }`, GitHub's authorize page to send the browser to; back at `/callback/github`, it links the account, then `callbackURL`. |
74
+ | `POST /auth/better-auth/sign-in/social` | `{ provider: "github", callbackURL }` | The same, signed out: signs in a visitor who linked that GitHub account. GitHub links to an existing account and signs a linked account in; it never makes one (`?error=signup_disabled`). |
75
+ | `GET /auth/better-auth/get-session` | | `{ session, user }`, `user.github` the linked login, or null when signed out. |
76
+
77
+ ```ts
78
+ const response = await fetch("/auth/better-auth/link-social", {
79
+ method: "POST",
80
+ headers: { "content-type": "application/json" },
81
+ body: JSON.stringify({ provider: "github", callbackURL: location.pathname }),
82
+ });
83
+ const { url } = await response.json();
84
+ window.location.assign(url); // GitHub, then back on this page with the account linked
85
+ ```
86
+
87
+ Each `callbackURL` (and `errorCallbackURL`, where an error goes as `?error=<code>`) is a path on the site's host. A GitHub account links to one visitor only (`?error=account_already_linked_to_different_user` for another), its email may differ from the visitor's, and GitHub must have verified it. The login is GitHub's answer for the account's own token when Better Auth links it (Better Auth sets no field a person may not set from a provider's profile), and is cleared when the visitor unlinks it (`POST /unlink-account { accountId }`, the account's id in `GET /list-accounts`). Linked, the account keeps no GitHub token: its login is read once, at each link or sign-in, and its tokens dropped, as a GitHub App's user token reaches what the App and the person both reach. A visitor links one GitHub account at most (another is `?error=unable_to_link_account`).
88
+
89
+ ## The staff sign-in
90
+
91
+ `betterAuthStaffLogin` is the app's sign-in for the company's own people on a host, which the `sanoma` bin uses off this machine (`staffLogin(betterAuthStaffLogin, { github })` from `@sanoma-ai/app`). Unlike the visitor login, nobody signs up: a code is sent, and an account made, only for an email the people file declares (anyone else gets the same answer, and nothing is stored or sent), or a GitHub login it declares. Its tables are in the schema `better_auth_staff`, migrated under the advisory lock `sanoma.login.better-auth.better_auth_staff`; its cookie is `sanoma.session_token`, for seven days, on whatever host the app answers on. Its ways in, under `/auth/better-auth/`:
92
+
93
+ - an email code (`/email-otp/send-verification-otp`, `/sign-in/email-otp`), with a sender;
94
+ - GitHub (`/sign-in/social`, back at `/callback/github`), with the company's GitHub App's client, matched by the verified email GitHub gives, else the login;
95
+ - an invite (`/magic-link/verify`), a one-use magic link valid 24 hours that the app mints server-side (`invite`), a new one replacing the person's last; Better Auth's own `/sign-in/magic-link` is not served.
96
+
97
+ Every other Better Auth route is a 404, a POST whose `Origin` is not the request's own host is a 403, and a `callbackURL`, `errorCallbackURL` or `newUserCallbackURL` that is not a path on the request's host is a 400. Invites are minted one at a time. Each sign-in is told to the app (`signin.recorded` in the ledger, with how).
98
+
61
99
  ### Later
62
100
 
63
- Email verification and password reset (they need a sender, such as Resend), social providers (Google, GitHub), and the rate limit kept in the database for several processes.
101
+ Password reset for visitors, more social providers (Google), and the rate limit kept in the database for several processes.
64
102
 
65
103
  The logo the app shows is Better Auth's mark, from the [Better Auth repository](https://github.com/better-auth/better-auth) (`demo/nextjs/components/logo.tsx`), in black or white. It is there only to identify the service.
package/dist/index.d.ts CHANGED
@@ -1,7 +1,10 @@
1
1
  export { betterAuthLogin } from "./login.ts";
2
+ export { betterAuthStaffLogin } from "./staff.ts";
2
3
  /**
3
4
  * Better Auth, a visitor login for public sites (`defineSite({ public: true, login: "better-auth" })`):
4
- * email and password, its users in schema `better_auth` on the company's database, its routes
5
- * under `/auth/better-auth/`, its cookies signed with the key the app derives for it.
5
+ * email and password, and an email code when the config names a sender, its users in schema
6
+ * `better_auth` on the company's database, its routes under `/auth/better-auth/`, its cookies
7
+ * signed with the key the app derives for it. The staff login is `betterAuthStaffLogin`, which the
8
+ * `sanoma` launcher runs on a host.
6
9
  */
7
10
  export declare const betterAuth: import("@sanoma-ai/workflows").Connector<"better-auth", {}>;
package/dist/index.js CHANGED
@@ -2,10 +2,13 @@ import { withSetup } from "@sanoma-ai/workflows";
2
2
  import { betterAuthOps } from "./connector.js";
3
3
  import { betterAuthLogin } from "./login.js";
4
4
  export { betterAuthLogin } from "./login.js";
5
+ export { betterAuthStaffLogin } from "./staff.js";
5
6
  /**
6
7
  * Better Auth, a visitor login for public sites (`defineSite({ public: true, login: "better-auth" })`):
7
- * email and password, its users in schema `better_auth` on the company's database, its routes
8
- * under `/auth/better-auth/`, its cookies signed with the key the app derives for it.
8
+ * email and password, and an email code when the config names a sender, its users in schema
9
+ * `better_auth` on the company's database, its routes under `/auth/better-auth/`, its cookies
10
+ * signed with the key the app derives for it. The staff login is `betterAuthStaffLogin`, which the
11
+ * `sanoma` launcher runs on a host.
9
12
  */
10
13
  export const betterAuth = withSetup(betterAuthOps, { login: betterAuthLogin });
11
14
  //# sourceMappingURL=index.js.map
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAC/C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAE7C,OAAO,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAC7C,OAAO,EAAE,oBAAoB,EAAE,MAAM,YAAY,CAAC;AAElD;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,SAAS,CAAC,aAAa,EAAE,EAAE,KAAK,EAAE,eAAe,EAAE,CAAC,CAAC"}