@pithy-sh/auth 0.1.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/LICENSE +21 -0
  2. package/README.md +46 -0
  3. package/docs/apple-signin.md +139 -0
  4. package/docs/facebook-oauth.md +92 -0
  5. package/docs/github-oauth.md +99 -0
  6. package/docs/google-oauth.md +118 -0
  7. package/package.json +58 -0
  8. package/pithy.manifest.json +108 -0
  9. package/src/admin/users.ts +357 -0
  10. package/src/audit/actions.ts +71 -0
  11. package/src/audit/emit.ts +223 -0
  12. package/src/capability.ts +300 -0
  13. package/src/client/api.ts +501 -0
  14. package/src/client/projection.ts +55 -0
  15. package/src/cloudflare-test.d.ts +16 -0
  16. package/src/data/betterAuth.ts +210 -0
  17. package/src/data/device.ts +57 -0
  18. package/src/data/kitFields.ts +69 -0
  19. package/src/data/rotatedToken.ts +40 -0
  20. package/src/data/tables.ts +38 -0
  21. package/src/device/registry.ts +139 -0
  22. package/src/email/send.ts +67 -0
  23. package/src/http/adminRoutes.ts +368 -0
  24. package/src/http/baseUrl.ts +109 -0
  25. package/src/http/csrf.ts +98 -0
  26. package/src/http/devLoginRoute.ts +159 -0
  27. package/src/http/errors.ts +70 -0
  28. package/src/http/guards.ts +158 -0
  29. package/src/http/middleware.ts +67 -0
  30. package/src/http/rateLimit.ts +36 -0
  31. package/src/http/resolve.ts +152 -0
  32. package/src/http/responses.ts +199 -0
  33. package/src/http/routes.ts +325 -0
  34. package/src/http/schemas.ts +118 -0
  35. package/src/http/views.ts +93 -0
  36. package/src/i18n/errorCopy.es.ts +35 -0
  37. package/src/i18n/errorCopy.ts +99 -0
  38. package/src/index.ts +24 -0
  39. package/src/instance/auth.ts +309 -0
  40. package/src/instance/plugins.ts +172 -0
  41. package/src/instance/providers.ts +185 -0
  42. package/src/instance/secrets.ts +197 -0
  43. package/src/migrations/0001_init.ts +229 -0
  44. package/src/migrations/pluginTables.ts +334 -0
  45. package/src/seeds/devSession.ts +286 -0
  46. package/src/seeds/example.ts +48 -0
  47. package/src/test-utils/liveApp.ts +338 -0
  48. package/src/token/rotation.ts +104 -0
  49. package/src/version.generated.ts +16 -0
@@ -0,0 +1,104 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ import { SQLiteDate } from "@pithy-sh/core/src/data/codecs";
5
+ import { RotatedToken } from "../data/rotatedToken";
6
+ import type { AuthDatabase } from "../data/tables";
7
+
8
+ /**
9
+ * Refresh-token rotation primitives: the atomic consume that makes rotation race-safe, and the
10
+ * reuse-detection ledger that turns a replayed refresh token into a family-wide revocation
11
+ * (OAuth refresh rotation, RFC 6819 §5.2.2.3). The HTTP route (`http/routes.ts`) composes these; they
12
+ * stay here as small, directly testable units over the shared Kysely.
13
+ */
14
+
15
+ /** The outcome of consuming a session: whether THIS call removed the row, and the consumed family. */
16
+ export interface ConsumeResult {
17
+ /** True when this call deleted the session row — the single winner of a concurrent rotation. */
18
+ won: boolean;
19
+ /** The consumed session's family id, or null when it had none (a fresh, never-rotated session). */
20
+ familyId: string | null;
21
+ }
22
+
23
+ /**
24
+ * Atomically consume a live session by its token. The conditional `DELETE ... RETURNING` is the single
25
+ * race gate: of N concurrent rotations presenting the same token, exactly one deletes the row and sees
26
+ * `won: true`; the rest see `won: false` and must not mint a successor. One presented token yields one
27
+ * successor.
28
+ */
29
+ export async function consumeSession(db: AuthDatabase, token: string): Promise<ConsumeResult> {
30
+ const row = await db
31
+ .deleteFrom("pithyAuthSessions")
32
+ .where("token", "=", token)
33
+ .returning("familyId")
34
+ .executeTakeFirst();
35
+ return row ? { won: true, familyId: row.familyId ?? null } : { won: false, familyId: null };
36
+ }
37
+
38
+ /**
39
+ * Record a consumed token in the reuse-detection ledger, tagged with the family it belonged to. Idempotent
40
+ * on the token PK — a redundant record (e.g. a retried write) is a no-op, never an error.
41
+ */
42
+ export async function recordConsumedToken(
43
+ db: AuthDatabase,
44
+ entry: { token: string; familyId: string; userId: string; rotatedAt: Date },
45
+ ): Promise<void> {
46
+ await db
47
+ .insertInto("pithyAuthRotatedTokens")
48
+ .values(RotatedToken.encode(entry))
49
+ .onConflict((oc) => oc.column("token").doNothing())
50
+ .execute();
51
+ }
52
+
53
+ /**
54
+ * The family/owner a consumed token belonged to and when it was consumed, or null when the token was
55
+ * never consumed. `rotatedAt` lets the caller tell a benign concurrent/retried rotation (a fresh
56
+ * consume) from a genuinely replayed refresh token (an old one).
57
+ */
58
+ export async function findConsumedToken(
59
+ db: AuthDatabase,
60
+ token: string,
61
+ ): Promise<{ familyId: string; userId: string; rotatedAt: Date } | null> {
62
+ const row = await db
63
+ .selectFrom("pithyAuthRotatedTokens")
64
+ .select(["familyId", "userId", "rotatedAt"])
65
+ .where("token", "=", token)
66
+ .executeTakeFirst();
67
+ return row ? { familyId: row.familyId, userId: row.userId, rotatedAt: SQLiteDate.parse(row.rotatedAt) } : null;
68
+ }
69
+
70
+ /** The tokens of every live session in a family — the set a family revocation must sign out. */
71
+ export async function familySessionTokens(db: AuthDatabase, familyId: string): Promise<string[]> {
72
+ const rows = await db.selectFrom("pithyAuthSessions").select("token").where("familyId", "=", familyId).execute();
73
+ return rows.map((row) => row.token);
74
+ }
75
+
76
+ /**
77
+ * Revoke an entire refresh-token family: sign out every live session sharing the id. Deletion goes
78
+ * through Better Auth's `deleteSession` (injected) so any adapter-side bookkeeping stays consistent —
79
+ * the same path device-revoke uses. Returns the number of sessions revoked.
80
+ */
81
+ export async function revokeFamily(
82
+ db: AuthDatabase,
83
+ familyId: string,
84
+ deleteSession: (token: string) => Promise<unknown>,
85
+ ): Promise<number> {
86
+ const tokens = await familySessionTokens(db, familyId);
87
+ for (const token of tokens) {
88
+ await deleteSession(token);
89
+ }
90
+ return tokens.length;
91
+ }
92
+
93
+ /**
94
+ * Prune ledger entries consumed before `cutoff`, bounding the table's growth. A token consumed longer
95
+ * ago than a full session lifetime can no longer match any live session, so its reuse-detection value
96
+ * has lapsed. The `rotatedAt` index keeps this an index range delete. Returns the number of rows removed.
97
+ */
98
+ export async function pruneConsumedTokens(db: AuthDatabase, cutoff: Date): Promise<number> {
99
+ const result = await db
100
+ .deleteFrom("pithyAuthRotatedTokens")
101
+ .where("rotatedAt", "<", SQLiteDate.encode(cutoff))
102
+ .executeTakeFirst();
103
+ return Number(result.numDeletedRows ?? 0n);
104
+ }
@@ -0,0 +1,16 @@
1
+ // SPDX-FileCopyrightText: 2026 Pithy
2
+ // SPDX-License-Identifier: MIT
3
+
4
+ // GENERATED by scripts/stampVersions.ts — do not edit by hand. Regenerate with `bun run stamp-versions`.
5
+ //
6
+ // A Worker cannot read its own package.json, so this is how @pithy-sh/auth knows its own version at
7
+ // runtime. The capability attaches it, and `GET /control-plane/manifest` reports it per capability —
8
+ // which is what answers "should this project upgrade" and "is this customer exposed to what we just
9
+ // fixed". Those questions are only answerable per module, because a project composes some capabilities
10
+ // and not others.
11
+
12
+ /** This package's npm name — the join key against a release feed. */
13
+ export const PACKAGE_NAME = "@pithy-sh/auth";
14
+
15
+ /** This package's version, stamped from its own package.json at generation time. */
16
+ export const PACKAGE_VERSION = "0.1.0";