@binary1702/auth 0.2.5 → 0.4.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.
Files changed (53) hide show
  1. package/AGENTS.md +155 -0
  2. package/CHANGELOG.md +49 -0
  3. package/INSTALL.md +27 -16
  4. package/PROMPT.md +325 -36
  5. package/README.md +338 -53
  6. package/dist/cli.js +248 -17
  7. package/dist/core/admin-html.d.ts +32 -0
  8. package/dist/core/admin-html.js +350 -0
  9. package/dist/core/admin.d.ts +56 -0
  10. package/dist/core/admin.js +90 -0
  11. package/dist/core/config.d.ts +10 -3
  12. package/dist/core/config.js +22 -8
  13. package/dist/core/db.d.ts +155 -10
  14. package/dist/core/db.js +449 -69
  15. package/dist/core/email.d.ts +49 -0
  16. package/dist/core/email.js +213 -0
  17. package/dist/core/guard.d.ts +52 -0
  18. package/dist/core/guard.js +106 -0
  19. package/dist/core/html.d.ts +43 -0
  20. package/dist/core/html.js +122 -28
  21. package/dist/core/invite.d.ts +7 -0
  22. package/dist/core/invite.js +30 -0
  23. package/dist/core/options.d.ts +95 -0
  24. package/dist/core/options.js +377 -0
  25. package/dist/core/org.d.ts +43 -0
  26. package/dist/core/org.js +65 -0
  27. package/dist/core/pages.d.ts +34 -0
  28. package/dist/core/pages.js +73 -0
  29. package/dist/core/password.d.ts +16 -0
  30. package/dist/core/password.js +41 -12
  31. package/dist/core/rate.d.ts +26 -0
  32. package/dist/core/rate.js +70 -0
  33. package/dist/core/reset.d.ts +9 -0
  34. package/dist/core/reset.js +31 -0
  35. package/dist/core/session.d.ts +11 -1
  36. package/dist/core/session.js +27 -7
  37. package/dist/core/signup.d.ts +13 -6
  38. package/dist/core/signup.js +30 -15
  39. package/dist/core/tokens.d.ts +29 -0
  40. package/dist/core/tokens.js +64 -0
  41. package/dist/core/verify.d.ts +8 -0
  42. package/dist/core/verify.js +33 -0
  43. package/dist/core/yaml.d.ts +10 -0
  44. package/dist/core/yaml.js +85 -0
  45. package/dist/index.d.ts +18 -6
  46. package/dist/index.js +18 -6
  47. package/dist/next/index.d.ts +10 -3
  48. package/dist/next/index.js +649 -63
  49. package/dist/next/proxy.d.ts +2 -1
  50. package/dist/next/proxy.js +22 -10
  51. package/dist/react/index.d.ts +72 -0
  52. package/dist/react/index.js +107 -0
  53. package/package.json +18 -4
@@ -1,5 +1,6 @@
1
1
  import { NextResponse, type NextRequest } from "next/server.js";
2
+ import { MATCHER } from "../core/guard.ts";
2
3
  /** Paste this into proxy.ts as `export const config = { matcher: MATCHER }` */
3
- export declare const MATCHER: string[];
4
+ export { MATCHER };
4
5
  export declare function proxy(request: NextRequest): Promise<NextResponse>;
5
6
  export default proxy;
@@ -2,10 +2,13 @@
2
2
  // Proxy (Next 16) / middleware (Next 15). Runs before every route.
3
3
  //
4
4
  // Verifies the cookie signature only. No database, no network.
5
- // no/invalid cookie + page request → 302 to /auth/sign-in?next=<path>
5
+ // no/invalid cookie + page request → 302 to the sign-in page?next=<path>
6
6
  // no/invalid cookie + /api request → 401 JSON
7
7
  // valid cookie → continue
8
8
  //
9
+ // The sign-in page is /auth/sign-in, or the app's own page when
10
+ // b17_auth.yaml names one under `pages:` (core/pages.ts).
11
+ //
9
12
  // A cheap first gate, not the last one: a revoked session (deleted
10
13
  // user, changed password) still has a valid signature and passes here.
11
14
  // getUser()/requireUser() check the database and refuse it, so no data
@@ -13,26 +16,35 @@
13
16
  //
14
17
  // The CLI writes proxy.ts with an inline `config.matcher`, because
15
18
  // Next reads the matcher statically and ignores a re-exported one.
16
- // This function also guards those paths itself, so a missing matcher
17
- // degrades to "slightly slower", never to "static assets blocked".
19
+ // The matcher only skips Next's build output; isPublic() (core/guard.ts)
20
+ // makes every other decision, so the rule lives in one tested place.
18
21
  // ─────────────────────────────────────────────────────────────
19
22
  import { NextResponse } from "next/server.js";
20
- import { COOKIE_NAME, SIGN_IN_PATH, getSecret } from "../core/config.js";
23
+ import { COOKIE_NAME, cookieName, getSecret } from "../core/config.js";
21
24
  import { verifySession } from "../core/session.js";
25
+ import { MATCHER, isPublic } from "../core/guard.js";
26
+ import { options } from "../core/options.js";
27
+ import { appPages, pagePath, sendsHome } from "../core/pages.js";
22
28
  /** Paste this into proxy.ts as `export const config = { matcher: MATCHER }` */
23
- export const MATCHER = ["/((?!auth|_next/static|_next/image|favicon.ico|.*\\..*).*)"];
24
- const PUBLIC = /^\/(auth(\/|$)|_next\/|favicon\.ico$)|\.[a-z0-9]+$/i;
29
+ export { MATCHER };
25
30
  export async function proxy(request) {
26
31
  const { pathname, search } = request.nextUrl;
27
- if (PUBLIC.test(pathname))
32
+ // `protected:` or `public:` from b17_auth.yaml, on top of the built-in rules (auth pages, Next internals)
33
+ const o = options();
34
+ // The current cookie name, then the pre-0.3.0 one: upgrading signs nobody out
35
+ const token = request.cookies.get(cookieName(o.production))?.value ?? request.cookies.get(COOKIE_NAME)?.value;
36
+ const signedIn = async () => !!token && !!(await verifySession(token, getSecret()));
37
+ // With `home:` set, the root is the public site and the app lives elsewhere: a signed-in visitor goes there
38
+ if (sendsHome(pathname, o.home) && (await signedIn()))
39
+ return NextResponse.redirect(new URL(o.home, request.url));
40
+ if (isPublic(pathname, { public: o.public, protected: o.protected, pages: appPages(o.pages) }))
28
41
  return NextResponse.next();
29
- const token = request.cookies.get(COOKIE_NAME)?.value;
30
- if (token && (await verifySession(token, getSecret())))
42
+ if (await signedIn())
31
43
  return NextResponse.next();
32
44
  if (pathname.startsWith("/api/")) {
33
45
  return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
34
46
  }
35
- const url = new URL(SIGN_IN_PATH, request.url);
47
+ const url = new URL(pagePath(o.pages, "signIn"), request.url);
36
48
  if (pathname !== "/")
37
49
  url.searchParams.set("next", pathname + search);
38
50
  return NextResponse.redirect(url);
@@ -0,0 +1,72 @@
1
+ import { type ReactNode } from "react";
2
+ import { type Authorization, type Organization, type Session } from "../core/org.ts";
3
+ import type { SignupPolicy } from "../core/signup.ts";
4
+ export type { Session, Organization, Authorization, SignupPolicy };
5
+ export type AuthState = {
6
+ user: Session["user"] | null;
7
+ organization: Organization | null;
8
+ authorization: Authorization;
9
+ /** True only while the provider is fetching the session itself */
10
+ loading: boolean;
11
+ /** Re-fetch GET /auth/session (after switching organization, for instance) */
12
+ refresh: () => Promise<void>;
13
+ /** May this user do that? For showing and hiding UI; the server still checks with requirePermission(). */
14
+ can: (permission: string) => boolean;
15
+ };
16
+ export declare function fetchSession(): Promise<Session | null>;
17
+ export declare function AuthProvider(props: {
18
+ session?: Session | null;
19
+ children: ReactNode;
20
+ }): import("react").FunctionComponentElement<import("react").ProviderProps<AuthState | null>>;
21
+ export declare function useAuth(): AuthState;
22
+ /**
23
+ * What a sign-in or sign-up attempt comes back with.
24
+ * ok, verify false signed in; go to `next` (checked by the server: same origin only)
25
+ * ok, verify true account created, no session yet: "check your email"
26
+ * not ok `error` is the message to show; `status` 401 wrong password,
27
+ * 403 cross-site / wrong invite / email not verified, 404 sign-up closed,
28
+ * 409 email taken, 429 too many tries (`retryAfter` seconds)
29
+ */
30
+ export type AuthResult = {
31
+ ok: true;
32
+ user: Session["user"];
33
+ next: string;
34
+ verify: boolean;
35
+ } | {
36
+ ok: false;
37
+ status: number;
38
+ error: string;
39
+ verify: boolean;
40
+ retryAfter?: number;
41
+ };
42
+ /** POST /auth/sign-in. On success the cookie is set; navigate to `next`. */
43
+ export declare function signIn(input: {
44
+ email: string;
45
+ password: string;
46
+ next?: string;
47
+ }): Promise<AuthResult>;
48
+ /** POST /auth/sign-up. `invite` when the policy requires it. */
49
+ export declare function signUp(input: {
50
+ email: string;
51
+ password: string;
52
+ invite?: string;
53
+ next?: string;
54
+ }): Promise<AuthResult>;
55
+ /** POST /auth/sign-out: clears the cookie. Then navigate somewhere public (or link to /auth/sign-out?next=/). */
56
+ export declare function signOut(): Promise<void>;
57
+ /** What a sign-in or sign-up page needs to know before it renders. */
58
+ export type Policy = {
59
+ /** May anyone sign up right now, is an invite code required, and is this the first account? */
60
+ signup: SignupPolicy;
61
+ /** Where the pages are: the app's own (b17_auth.yaml `pages:`) or the package's */
62
+ pages: {
63
+ signIn: string;
64
+ signUp: string;
65
+ };
66
+ /** The signed-in home (b17_auth.yaml `home:`), else "/" */
67
+ home: string;
68
+ /** The app name from b17_auth.yaml, if set */
69
+ name?: string;
70
+ };
71
+ /** GET /auth/policy */
72
+ export declare function fetchPolicy(): Promise<Policy>;
@@ -0,0 +1,107 @@
1
+ "use client";
2
+ // ─────────────────────────────────────────────────────────────
3
+ // React side of the session contract. For rendering only: every
4
+ // protected operation is still checked on the server.
5
+ //
6
+ // <AuthProvider session={await getSession()}> in a server layout,
7
+ // no request from the client
8
+ // <AuthProvider> anywhere else: fetches
9
+ // GET /auth/session once
10
+ // const { user, organization, authorization } = useAuth()
11
+ //
12
+ // Signed out: user and organization are null, permissions are empty.
13
+ //
14
+ // For an app that draws its own sign-in / sign-up pages (b17_auth.yaml
15
+ // `pages:`), the protocol as functions. The package decides what is
16
+ // valid, where to go next and when to refuse; the page only renders:
17
+ //
18
+ // const r = await signIn({ email, password, next })
19
+ // r.ok ? location.assign(r.next) : setError(r.error)
20
+ //
21
+ // const { signup } = await fetchPolicy() // may anyone sign up right now? first account?
22
+ // ─────────────────────────────────────────────────────────────
23
+ import { createContext, createElement, useCallback, useContext, useEffect, useState } from "react";
24
+ import { NO_SESSION, hasPermission } from "../core/org.js";
25
+ const SESSION_URL = "/auth/session";
26
+ const SIGN_IN_URL = "/auth/sign-in";
27
+ const SIGN_UP_URL = "/auth/sign-up";
28
+ const SIGN_OUT_URL = "/auth/sign-out";
29
+ const POLICY_URL = "/auth/policy";
30
+ const Ctx = createContext(null);
31
+ export async function fetchSession() {
32
+ const res = await fetch(SESSION_URL, { credentials: "same-origin", cache: "no-store" });
33
+ return res.ok ? (await res.json()) : null;
34
+ }
35
+ export function AuthProvider(props) {
36
+ const provided = props.session !== undefined;
37
+ const [session, setSession] = useState(props.session ?? null);
38
+ const [loading, setLoading] = useState(!provided);
39
+ const refresh = useCallback(async () => {
40
+ setLoading(true);
41
+ try {
42
+ setSession(await fetchSession());
43
+ }
44
+ finally {
45
+ setLoading(false);
46
+ }
47
+ }, []);
48
+ useEffect(() => {
49
+ if (provided)
50
+ setSession(props.session ?? null);
51
+ else
52
+ void refresh();
53
+ }, [provided, props.session, refresh]);
54
+ const authorization = session?.authorization ?? NO_SESSION.authorization;
55
+ const value = {
56
+ user: session?.user ?? NO_SESSION.user,
57
+ organization: session?.organization ?? NO_SESSION.organization,
58
+ authorization,
59
+ loading,
60
+ refresh,
61
+ can: (permission) => hasPermission({ authorization }, permission),
62
+ };
63
+ return createElement(Ctx.Provider, { value }, props.children);
64
+ }
65
+ export function useAuth() {
66
+ const ctx = useContext(Ctx);
67
+ if (!ctx)
68
+ throw new Error("useAuth() needs an <AuthProvider> above it");
69
+ return ctx;
70
+ }
71
+ async function attempt(url, body) {
72
+ const res = await fetch(url, {
73
+ method: "POST",
74
+ credentials: "same-origin",
75
+ headers: { "content-type": "application/json" },
76
+ body: JSON.stringify(body),
77
+ });
78
+ const data = (await res.json().catch(() => ({})));
79
+ if (res.ok && data.user)
80
+ return { ok: true, user: data.user, next: data.next ?? "/", verify: data.verify === true };
81
+ return {
82
+ ok: false,
83
+ status: res.status,
84
+ error: data.error ?? (data.verify ? "Check your email for a verification link, then sign in." : "Something went wrong. Try again."),
85
+ verify: data.verify === true,
86
+ retryAfter: data.retryAfter,
87
+ };
88
+ }
89
+ /** POST /auth/sign-in. On success the cookie is set; navigate to `next`. */
90
+ export function signIn(input) {
91
+ return attempt(SIGN_IN_URL, input);
92
+ }
93
+ /** POST /auth/sign-up. `invite` when the policy requires it. */
94
+ export function signUp(input) {
95
+ return attempt(SIGN_UP_URL, input);
96
+ }
97
+ /** POST /auth/sign-out: clears the cookie. Then navigate somewhere public (or link to /auth/sign-out?next=/). */
98
+ export async function signOut() {
99
+ await fetch(SIGN_OUT_URL, { method: "POST", credentials: "same-origin", headers: { "content-type": "application/json" }, body: "{}" });
100
+ }
101
+ /** GET /auth/policy */
102
+ export async function fetchPolicy() {
103
+ const res = await fetch(POLICY_URL, { credentials: "same-origin", cache: "no-store" });
104
+ if (!res.ok)
105
+ throw new Error(`GET ${POLICY_URL} answered ${res.status}`);
106
+ return (await res.json());
107
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@binary1702/auth",
3
- "version": "0.2.5",
3
+ "version": "0.4.0",
4
4
  "description": "Email + password auth for Next.js. One command: npx @binary1702/auth",
5
5
  "type": "module",
6
6
  "repository": {
@@ -26,27 +26,41 @@
26
26
  "./next/proxy": {
27
27
  "types": "./dist/next/proxy.d.ts",
28
28
  "import": "./dist/next/proxy.js"
29
+ },
30
+ "./react": {
31
+ "types": "./dist/react/index.d.ts",
32
+ "import": "./dist/react/index.js"
29
33
  }
30
34
  },
31
35
  "files": [
32
36
  "dist",
33
37
  "INSTALL.md",
34
- "PROMPT.md"
38
+ "PROMPT.md",
39
+ "AGENTS.md",
40
+ "CHANGELOG.md"
35
41
  ],
36
42
  "scripts": {
37
43
  "build": "tsc",
38
44
  "prepublishOnly": "npm run build",
39
- "test": "node --test tests/*.test.ts"
45
+ "test": "node --test tests/*.test.ts",
46
+ "dev": "npx next dev demo -p 3002"
40
47
  },
41
48
  "dependencies": {
42
49
  "@neondatabase/serverless": "^1.1.0",
43
50
  "jose": "^6.2.5"
44
51
  },
45
52
  "peerDependencies": {
46
- "next": ">=15.0.0"
53
+ "next": ">=15.0.0",
54
+ "react": ">=18"
55
+ },
56
+ "peerDependenciesMeta": {
57
+ "react": {
58
+ "optional": true
59
+ }
47
60
  },
48
61
  "devDependencies": {
49
62
  "@types/node": "^20",
63
+ "@types/react": "19.3.0",
50
64
  "next": "^16.3.2",
51
65
  "typescript": "^5.9.3"
52
66
  },