@binary1702/auth 0.2.5 → 0.3.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 (49) hide show
  1. package/AGENTS.md +139 -0
  2. package/CHANGELOG.md +31 -0
  3. package/INSTALL.md +27 -16
  4. package/PROMPT.md +324 -36
  5. package/README.md +279 -51
  6. package/dist/cli.js +232 -17
  7. package/dist/core/admin-html.d.ts +26 -0
  8. package/dist/core/admin-html.js +252 -0
  9. package/dist/core/admin.d.ts +26 -0
  10. package/dist/core/admin.js +48 -0
  11. package/dist/core/config.d.ts +8 -1
  12. package/dist/core/config.js +20 -6
  13. package/dist/core/db.d.ts +131 -10
  14. package/dist/core/db.js +388 -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 +49 -0
  18. package/dist/core/guard.js +103 -0
  19. package/dist/core/html.d.ts +36 -0
  20. package/dist/core/html.js +106 -25
  21. package/dist/core/options.d.ts +76 -0
  22. package/dist/core/options.js +310 -0
  23. package/dist/core/org.d.ts +34 -0
  24. package/dist/core/org.js +55 -0
  25. package/dist/core/password.d.ts +16 -0
  26. package/dist/core/password.js +41 -12
  27. package/dist/core/rate.d.ts +26 -0
  28. package/dist/core/rate.js +70 -0
  29. package/dist/core/reset.d.ts +9 -0
  30. package/dist/core/reset.js +31 -0
  31. package/dist/core/session.d.ts +11 -1
  32. package/dist/core/session.js +27 -7
  33. package/dist/core/signup.d.ts +9 -6
  34. package/dist/core/signup.js +26 -15
  35. package/dist/core/tokens.d.ts +29 -0
  36. package/dist/core/tokens.js +64 -0
  37. package/dist/core/verify.d.ts +8 -0
  38. package/dist/core/verify.js +33 -0
  39. package/dist/core/yaml.d.ts +10 -0
  40. package/dist/core/yaml.js +85 -0
  41. package/dist/index.d.ts +17 -6
  42. package/dist/index.js +17 -6
  43. package/dist/next/index.d.ts +10 -3
  44. package/dist/next/index.js +444 -52
  45. package/dist/next/proxy.d.ts +2 -1
  46. package/dist/next/proxy.js +11 -7
  47. package/dist/react/index.d.ts +18 -0
  48. package/dist/react/index.js +55 -0
  49. package/package.json +16 -3
@@ -0,0 +1,26 @@
1
+ import type { Session } from "./org.ts";
2
+ export declare const ADMIN_PATH = "/auth/admin";
3
+ export declare function isAdmin(session: Pick<Session, "user"> | null, admins: readonly string[]): boolean;
4
+ /** The forms on the page. Each is one POST with `action` and a few fields. */
5
+ export declare const USER_ACTIONS: readonly ["add", "set-password", "sign-out", "verify", "send-verify", "remove"];
6
+ export declare const ORG_ACTIONS: readonly ["create", "add-member", "set-role", "remove-member", "remove-org"];
7
+ export type UserAction = (typeof USER_ACTIONS)[number];
8
+ export type OrgAction = (typeof ORG_ACTIONS)[number];
9
+ export declare const isUserAction: (a: string) => a is UserAction;
10
+ export declare const isOrgAction: (a: string) => a is OrgAction;
11
+ /**
12
+ * What an admin may not do to themselves: remove or lock out the account
13
+ * they are using. (Their own password change is fine: it ends their other
14
+ * sessions, and the page signs them in again.)
15
+ */
16
+ export declare function refusedOnSelf(action: UserAction, targetEmail: string, selfEmail: string): string | null;
17
+ /**
18
+ * Where to land after an action: the view the form was on. Only two shapes
19
+ * exist, so the value is rebuilt from parts, never taken from the form as is.
20
+ */
21
+ export declare function backTo(fields: {
22
+ org?: string;
23
+ tab?: string;
24
+ }): string;
25
+ /** Roles the page offers in its dropdowns: whatever the yaml defines, plus any role already in use. */
26
+ export declare function roleChoices(defined: readonly string[], inUse: readonly string[]): string[];
@@ -0,0 +1,48 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // The admin page's rules. Pure.
3
+ //
4
+ // admins: ben@example.com … (b17_auth.yaml, or AUTH_ADMINS) may open
5
+ // /auth/admin and do what the CLI does:
6
+ // users and memberships. Never the yaml.
7
+ //
8
+ // An admin is an email, not a role: a role lives in one organization,
9
+ // the admin page spans all of them.
10
+ // ─────────────────────────────────────────────────────────────
11
+ export const ADMIN_PATH = "/auth/admin";
12
+ export function isAdmin(session, admins) {
13
+ return !!session && admins.includes(session.user.email.toLowerCase());
14
+ }
15
+ /** The forms on the page. Each is one POST with `action` and a few fields. */
16
+ export const USER_ACTIONS = ["add", "set-password", "sign-out", "verify", "send-verify", "remove"];
17
+ export const ORG_ACTIONS = ["create", "add-member", "set-role", "remove-member", "remove-org"];
18
+ export const isUserAction = (a) => USER_ACTIONS.includes(a);
19
+ export const isOrgAction = (a) => ORG_ACTIONS.includes(a);
20
+ /**
21
+ * What an admin may not do to themselves: remove or lock out the account
22
+ * they are using. (Their own password change is fine: it ends their other
23
+ * sessions, and the page signs them in again.)
24
+ */
25
+ export function refusedOnSelf(action, targetEmail, selfEmail) {
26
+ if (targetEmail.toLowerCase() !== selfEmail.toLowerCase())
27
+ return null;
28
+ if (action === "remove")
29
+ return "You cannot remove the account you are signed in with.";
30
+ if (action === "sign-out")
31
+ return "Use /auth/sign-out to end your own session.";
32
+ return null;
33
+ }
34
+ /**
35
+ * Where to land after an action: the view the form was on. Only two shapes
36
+ * exist, so the value is rebuilt from parts, never taken from the form as is.
37
+ */
38
+ export function backTo(fields) {
39
+ if (fields.org && /^[0-9a-f-]{36}$/i.test(fields.org))
40
+ return `org=${fields.org}`;
41
+ if (fields.tab === "users" || fields.tab === "settings")
42
+ return `tab=${fields.tab}`;
43
+ return "";
44
+ }
45
+ /** Roles the page offers in its dropdowns: whatever the yaml defines, plus any role already in use. */
46
+ export function roleChoices(defined, inUse) {
47
+ return [...new Set(["owner", "admin", "member", ...defined, ...inUse])];
48
+ }
@@ -1,8 +1,15 @@
1
1
  export declare const COOKIE_NAME = "auth_session";
2
+ /**
3
+ * In production the cookie carries the __Host- prefix: the browser then
4
+ * refuses it unless it is Secure, has Path=/ and no Domain, so a
5
+ * subdomain or a plain-http page cannot plant one. Pure.
6
+ */
7
+ export declare function cookieName(production: boolean): string;
8
+ /** The default; b17_auth.yaml `sessionDays` overrides it */
2
9
  export declare const SESSION_DAYS = 7;
3
10
  export declare const SIGN_IN_PATH = "/auth/sign-in";
4
11
  export declare const SIGN_OUT_PATH = "/auth/sign-out";
5
12
  export declare const SIGN_UP_PATH = "/auth/sign-up";
6
- export declare const TABLE = "auth_users";
13
+ export declare const TABLE = "users";
7
14
  export declare function getSecret(): string;
8
15
  export declare function getDatabaseUrl(): string;
@@ -1,24 +1,38 @@
1
1
  // ─────────────────────────────────────────────────────────────
2
- // Configuration. Two env vars, nothing else.
2
+ // Constants, and the two values nothing works without.
3
3
  //
4
- // DATABASE_URL Postgres connection string (Neon)
5
- // AUTH_SECRET signs the session cookie; generated by the CLI
4
+ // Everything configurable comes from core/options.ts: b17_auth.yaml
5
+ // with ${NAME} references, or the env vars on their own:
6
+ //
7
+ // DATABASE_URL Postgres connection string (Neon) required
8
+ // AUTH_SECRET signs the session cookie generated by the CLI
9
+ // the rest see core/options.ts
6
10
  // ─────────────────────────────────────────────────────────────
11
+ import { options } from "./options.js";
7
12
  export const COOKIE_NAME = "auth_session";
13
+ /**
14
+ * In production the cookie carries the __Host- prefix: the browser then
15
+ * refuses it unless it is Secure, has Path=/ and no Domain, so a
16
+ * subdomain or a plain-http page cannot plant one. Pure.
17
+ */
18
+ export function cookieName(production) {
19
+ return production ? `__Host-${COOKIE_NAME}` : COOKIE_NAME;
20
+ }
21
+ /** The default; b17_auth.yaml `sessionDays` overrides it */
8
22
  export const SESSION_DAYS = 7;
9
23
  export const SIGN_IN_PATH = "/auth/sign-in";
10
24
  export const SIGN_OUT_PATH = "/auth/sign-out";
11
25
  export const SIGN_UP_PATH = "/auth/sign-up";
12
- export const TABLE = "auth_users";
26
+ export const TABLE = "users";
13
27
  export function getSecret() {
14
- const secret = process.env.AUTH_SECRET;
28
+ const secret = options().secret;
15
29
  if (!secret || secret.length < 16) {
16
30
  throw new Error("AUTH_SECRET is missing. Run: npx @binary1702/auth");
17
31
  }
18
32
  return secret;
19
33
  }
20
34
  export function getDatabaseUrl() {
21
- const url = process.env.DATABASE_URL;
35
+ const url = options().database;
22
36
  if (!url) {
23
37
  throw new Error("DATABASE_URL is missing. Run: npx @binary1702/auth");
24
38
  }
package/dist/core/db.d.ts CHANGED
@@ -1,26 +1,147 @@
1
1
  import { TABLE } from "./config.ts";
2
2
  import type { User } from "./session.ts";
3
- export declare function ensureSchema(): Promise<void>;
4
- type Row = {
3
+ export type Statement = {
4
+ text: string;
5
+ params?: unknown[];
6
+ };
7
+ /** What the package needs from a database: parameterised queries, and a transaction if it has one. */
8
+ export interface Db {
9
+ query<T = Record<string, unknown>>(text: string, params?: unknown[]): Promise<T[]>;
10
+ /** Run statements in one transaction. A store without it runs them one by one. */
11
+ transaction?(statements: Statement[]): Promise<void>;
12
+ }
13
+ /** The real store, created once per process from DATABASE_URL. */
14
+ export declare function db(): Db;
15
+ export type Migration = {
16
+ id: number;
17
+ name: string;
18
+ statements: string[];
19
+ };
20
+ /**
21
+ * Ordered, append-only. Never edit a step that has shipped: add a new
22
+ * one. Step 1 is the 0.2.5 schema exactly, so an existing install
23
+ * applies it as a no-op through IF NOT EXISTS.
24
+ */
25
+ export declare const MIGRATIONS: Migration[];
26
+ /**
27
+ * The package's tables have plain names (users, orgs, members, tokens).
28
+ * A database that already holds a `users` table of its own would be
29
+ * silently shared by CREATE TABLE IF NOT EXISTS, so a fresh install
30
+ * checks first. Pure: decides from what the store reports.
31
+ */
32
+ export declare function foreignUsersTable(columns: string[]): boolean;
33
+ export declare const FOREIGN_USERS_TABLE = "This database already has a `users` table that is not this package's. Point DATABASE_URL at a database (or Postgres schema) of its own.";
34
+ /** The steps still to run, in order. Pure. */
35
+ export declare function pendingSteps(applied: number[], steps?: Migration[]): Migration[];
36
+ /** Everything a pending step needs to run, including recording that it ran. Pure. */
37
+ export declare function migrationStatements(steps: Migration[]): Statement[];
38
+ /**
39
+ * Apply pending migrations. Memoized per store and per process, so
40
+ * route handlers can call it on every request for free after the first.
41
+ * A failure clears the memo so the next call retries.
42
+ */
43
+ export declare function ensureSchema(store?: Db): Promise<void>;
44
+ export type UserRow = {
5
45
  id: string;
6
46
  email: string;
7
47
  password_hash: string;
8
48
  epoch: number;
49
+ /** When the email was verified, or null */
50
+ verified_at: string | Date | null;
9
51
  };
10
- export declare function findUser(email: string): Promise<Row | null>;
11
- export declare function createUser(email: string, passwordHash: string): Promise<User & {
52
+ export declare function findUser(email: string, store?: Db): Promise<UserRow | null>;
53
+ /** Two sign-ups raced on one email and the second lost: the handler answers 409. */
54
+ export declare class DuplicateUserError extends Error {
55
+ constructor(email: string);
56
+ }
57
+ /** Postgres's unique_violation */
58
+ export declare const isUniqueViolation: (err: unknown) => boolean;
59
+ export declare function createUser(email: string, passwordHash: string, opts?: {
60
+ verified?: boolean;
61
+ }, store?: Db): Promise<User & {
12
62
  epoch: number;
63
+ verified: boolean;
13
64
  }>;
65
+ /** Replace a hash with one made under stronger parameters. No epoch bump: nothing about the session changes. */
66
+ export declare function rehashPassword(id: string, passwordHash: string, store?: Db): Promise<void>;
67
+ /** What a session check needs about the user: current epoch and verification. Null if the user is gone. */
68
+ export type UserState = {
69
+ epoch: number;
70
+ verified: boolean;
71
+ };
72
+ export declare function userState(id: string, store?: Db): Promise<UserState | null>;
14
73
  /** The user's current session_epoch, or null if the user is gone */
15
- export declare function sessionEpoch(id: string): Promise<number | null>;
74
+ export declare function sessionEpoch(id: string, store?: Db): Promise<number | null>;
75
+ /** Everything a session needs, in one query: the user's state and their active organization. */
76
+ export type SessionRow = UserState & {
77
+ id: string;
78
+ email: string;
79
+ org_id: string | null;
80
+ org_name: string | null;
81
+ role: string | null;
82
+ };
83
+ export declare function sessionRow(id: string, store?: Db): Promise<SessionRow | null>;
84
+ export type OrgRow = {
85
+ id: string;
86
+ name: string;
87
+ created_at: string;
88
+ };
89
+ export declare function createOrg(name: string, store?: Db): Promise<OrgRow>;
90
+ /** By id (UUID) or exact name. Null if there is no such organization. */
91
+ export declare function findOrg(idOrName: string, store?: Db): Promise<OrgRow | null>;
92
+ export declare function listOrgs(store?: Db): Promise<(OrgRow & {
93
+ members: number;
94
+ })[]>;
95
+ /** Add or update a membership. Becomes the user's active org if they had none. */
96
+ export declare function addMember(orgId: string, userId: string, role: string, store?: Db): Promise<void>;
97
+ export declare function removeMember(orgId: string, userId: string, store?: Db): Promise<boolean>;
98
+ export declare function removeOrg(orgId: string, store?: Db): Promise<boolean>;
99
+ export type Membership = {
100
+ org_id: string;
101
+ org: string;
102
+ role: string;
103
+ };
104
+ export type AdminUser = {
105
+ id: string;
106
+ email: string;
107
+ created_at: string;
108
+ verified: boolean;
109
+ memberships: Membership[];
110
+ };
111
+ export type AdminOrg = {
112
+ id: string;
113
+ name: string;
114
+ created_at: string;
115
+ members: {
116
+ user_id: string;
117
+ email: string;
118
+ role: string;
119
+ }[];
120
+ };
121
+ /** Every user with their memberships, one query. */
122
+ export declare function adminUsers(store?: Db): Promise<AdminUser[]>;
123
+ /** Every organization with its members, one query. */
124
+ export declare function adminOrgs(store?: Db): Promise<AdminOrg[]>;
125
+ /** Switch the user's active organization. False unless they are a member of it: the client's choice is checked, never trusted. */
126
+ export declare function setActiveOrg(userId: string, orgId: string, store?: Db): Promise<boolean>;
127
+ /** Record that the user followed the verification link. Idempotent. Null if the user is gone. */
128
+ export declare function markVerified(id: string, store?: Db): Promise<User | null>;
16
129
  /** Revoke every session of this user. False if there is no such user. */
17
- export declare function revokeSessions(email: string): Promise<boolean>;
130
+ export declare function revokeSessions(email: string, store?: Db): Promise<boolean>;
18
131
  /** Delete the user. Their sessions die with the row. False if none. */
19
- export declare function removeUser(email: string): Promise<boolean>;
20
- export declare function countUsers(): Promise<number>;
21
- export declare function listUsers(): Promise<(User & {
132
+ export declare function removeUser(email: string, store?: Db): Promise<boolean>;
133
+ export declare function countUsers(store?: Db): Promise<number>;
134
+ export declare function listUsers(store?: Db): Promise<(User & {
22
135
  created_at: string;
23
136
  })[]>;
24
137
  /** Change a password. Also revokes every session, old password's included. */
25
- export declare function setPassword(email: string, passwordHash: string): Promise<boolean>;
138
+ export declare function setPassword(email: string, passwordHash: string, store?: Db): Promise<boolean>;
139
+ /**
140
+ * Same, by id, for a reset link. Also marks the email verified: following
141
+ * the link proved the address. Returns the user with the new epoch so the
142
+ * caller can sign them in; null if the user is gone.
143
+ */
144
+ export declare function setPasswordById(id: string, passwordHash: string, store?: Db): Promise<(User & {
145
+ epoch: number;
146
+ }) | null>;
26
147
  export { TABLE };