@shtora/auth 0.0.0-stage → 0.1.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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 airshakur88
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,235 @@
1
- # Temporary Holding Version
2
-
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
1
+ # @shtora/auth
2
+
3
+ [Repository](https://github.com/orgShtora-1/auth) · [Issues](https://github.com/orgShtora-1/auth/issues)
4
+
5
+ Framework-neutral authentication handlers and session management for Next.js and Cloudflare Workers. The core uses standard `Request`/`Response` APIs and Web Crypto; optional entry points add Cloudflare D1, OIDC, and WebAuthn support.
6
+
7
+ Your application owns its user records, credential verification, password hashing, email delivery, and persistent stores. The package does not hash, encrypt, or persist passwords and includes no ORM.
8
+
9
+ ## Install
10
+
11
+ Install the published package from npm:
12
+
13
+ ```sh
14
+ bun add @shtora/auth@latest
15
+ ```
16
+
17
+ To install a beta release:
18
+
19
+ ```sh
20
+ bun add @shtora/auth@beta
21
+ ```
22
+
23
+ The `beta` tag must point to a published beta version. It is not currently available.
24
+
25
+ Install optional protocol dependencies only when you use those entry points:
26
+
27
+ ```sh
28
+ bun add openid-client # @shtora/auth/oidc
29
+ bun add @simplewebauthn/server # @shtora/auth/webauthn
30
+ ```
31
+
32
+ ## Next.js App Router
33
+
34
+ Create the handler with application-owned adapters. `users`, `credentials`, `sessions`, `rateLimiter`, and `resolveClientIp` below are implementations in your application, not exports from this package.
35
+
36
+ ```ts
37
+ // src/server/auth.ts
38
+ import { createAuth } from "@shtora/auth";
39
+ import {
40
+ blockedEmails,
41
+ blockedIps,
42
+ credentials,
43
+ rateLimiter,
44
+ resolveClientIp,
45
+ sessions,
46
+ users,
47
+ } from "./auth-adapters";
48
+
49
+ export const auth = createAuth({
50
+ users,
51
+ credentials,
52
+ sessions,
53
+ rateLimiter,
54
+ resolveClientIp,
55
+ allowedHosts: ["app.example.com"],
56
+ emailPolicy: {
57
+ async allow({ value }) {
58
+ return !blockedEmails.has(value);
59
+ },
60
+ },
61
+ ipPolicy: {
62
+ async allow({ value }) {
63
+ return !blockedIps.has(value);
64
+ },
65
+ },
66
+ });
67
+ ```
68
+
69
+ Forward the supported methods from a Route Handler:
70
+
71
+ ```ts
72
+ // app/api/auth/[...auth]/route.ts
73
+ import { auth } from "@/server/auth";
74
+
75
+ export const GET = (request: Request) => auth.handle(request);
76
+ export const POST = (request: Request) => auth.handle(request);
77
+ export const DELETE = (request: Request) => auth.handle(request);
78
+ ```
79
+
80
+ Use the real hostname in `allowedHosts`. Resolve client IPs only from a trusted hosting-platform or proxy value; do not parse arbitrary `X-Forwarded-For` input. Configure a shared, persistent rate limiter for public authentication endpoints.
81
+
82
+ ## Cloudflare Workers and D1
83
+
84
+ Import D1 adapters from the dedicated entry point. The application still supplies its own user and credential adapters; D1 stores sessions and rate-limit counters in this example.
85
+
86
+ ```ts
87
+ import { createAuth } from "@shtora/auth";
88
+ import {
89
+ cleanupD1AuthState,
90
+ createD1RateLimiter,
91
+ createD1SessionStore,
92
+ type D1Database,
93
+ } from "@shtora/auth/cloudflare";
94
+ import { credentials, users } from "./auth-adapters";
95
+
96
+ interface Env {
97
+ DB: D1Database;
98
+ }
99
+
100
+ let auth: ReturnType<typeof createAuth> | undefined;
101
+
102
+ function getAuth(env: Env) {
103
+ return auth ??= createAuth({
104
+ users,
105
+ credentials,
106
+ sessions: createD1SessionStore(env.DB),
107
+ rateLimiter: createD1RateLimiter(env.DB, {
108
+ identifier: { limit: 5, windowSeconds: 60 },
109
+ ip: { limit: 100, windowSeconds: 60 },
110
+ }),
111
+ resolveClientIp: (request) => request.headers.get("cf-connecting-ip"),
112
+ allowedHosts: ["auth.example.com"],
113
+ });
114
+ }
115
+
116
+ export default {
117
+ fetch(request: Request, env: Env): Promise<Response> {
118
+ return getAuth(env).handle(request);
119
+ },
120
+
121
+ async scheduled(_controller: unknown, env: Env): Promise<void> {
122
+ await cleanupD1AuthState(env.DB, { rateLimitMaxWindowSeconds: 60 });
123
+ },
124
+ };
125
+ ```
126
+
127
+ The npm package omits SQL files; see the [Cloudflare D1 adapter](#cloudflare-d1-adapter) section for the pinned migration source and application instructions. D1 cleanup is bounded; schedule it often enough for your session and rate-limit volume. Workers Free and Paid use the same package code, subject to each plan's D1, storage, and CPU quotas.
128
+
129
+ ## Routes
130
+
131
+ The default base path is `/api/auth`; set `basePath` to change it.
132
+
133
+ | Method | Path | Behavior |
134
+ | --- | --- | --- |
135
+ | `POST` | `/api/auth/register` | Creates an account. Duplicate and policy-blocked registrations share the same `202` response. |
136
+ | `POST` | `/api/auth/login` | Verifies credentials and sets a session cookie. |
137
+ | `POST` | `/api/auth/logout` | Revokes the current session and clears the cookie after successful revocation. |
138
+ | `GET` | `/api/auth/session` | Returns the authenticated user or `401`. |
139
+ | `GET` | `/api/auth/sessions` | Lists the user's active sessions without token hashes. |
140
+ | `DELETE` | `/api/auth/sessions/:id` | Revokes a session owned by the current user. |
141
+ | `DELETE` | `/api/auth/sessions` | Revokes all sessions for the current user. |
142
+ | `POST` | `/api/auth/email-verification/request` | Optional; sends a verification message with an enumeration-resistant response. |
143
+ | `POST` | `/api/auth/email-verification/confirm` | Optional; consumes a one-time token and marks the account verified. |
144
+ | `POST` | `/api/auth/password-reset/request` | Optional; sends a reset message with an enumeration-resistant response. |
145
+ | `POST` | `/api/auth/password-reset/confirm` | Optional; revokes sessions, then delegates the password update to the application. |
146
+
147
+ State-changing POST and DELETE requests require a same-origin `Origin` header by default. Read-only session GET requests allow a missing `Origin`. Keep this check enabled for browser cookie authentication unless equivalent CSRF protection is in place.
148
+ ## Registration and login identifiers
149
+
150
+ Email-only behavior is the default. `identity.registration` accepts `"email"`, `"username"`, `"both"`, or `"either"`; `"either"` requires exactly one identifier. `identity.login` accepts `"email"`, `"username"`, or `"either"`, also requiring exactly one login identifier. Compatible pairs are email/email, username/username, either/either, and both with any login mode; other pairs fail during `createAuth`.
151
+
152
+ ```ts
153
+ createAuth({
154
+ users,
155
+ credentials,
156
+ sessions,
157
+ identity: { registration: "either", login: "either" },
158
+ passwordPolicy: {
159
+ minLength: 12,
160
+ maxLength: 128,
161
+ requireUppercase: true,
162
+ requireLowercase: true,
163
+ requireSpecial: true,
164
+ },
165
+ });
166
+ ```
167
+
168
+ Registration accepts `{ email, password }`, `{ username, password }`, both identifiers with `registration: "both"`, or exactly one identifier with `"either"`. Login accepts the configured identifier field and a password; `"either"` rejects requests containing both. Unknown JSON keys are ignored, but supplying a disabled email/username field is invalid. Password is always required.
169
+
170
+ Usernames are 3–32 ASCII letters, digits, `.`, `_`, or `-`; they are canonicalized to lowercase without trimming. The application’s `UserStore.findByIdentifier` must use the same canonical form, and `create` must enforce email/username uniqueness atomically with database constraints. Email input is lowercased; whitespace is rejected. Username syntax excludes `@`, so valid email and username values cannot collide across namespaces. Lookup and rate limiting carry an explicit `"email"`/`"username"` discriminator.
171
+
172
+ `AuthUser` contains an opaque `id` and optional `email`/`username`. The package returns only `id` and present email/username fields; it never spreads adapter fields into a public response. OIDC and WebAuthn continue to map identities through the application’s opaque user ID.
173
+
174
+ `UserStore` results must contain exactly the configured identity shape before a session is created: email mode requires only email, username mode only username, both requires both, and either requires exactly one. Missing or invalid canonical identifiers fail closed and are reported through `onError`; no partial identity response is issued.
175
+
176
+ The default password policy is min 1 and max 4096 Unicode code points, with uppercase/lowercase/special requirements disabled. Policy checks run during registration and password-reset confirmation, not login. Passwords are neither trimmed nor normalized. “Special” means a Unicode code point that is not a letter, number, or whitespace. The 4096-code-point hard cap is independent of the request-body byte limit. Invalid shape or policy returns the generic `400 { "error": "invalid_request" }`.
177
+
178
+ Email verification requires registration mode `"email"` or `"both"` and rejects incompatible configurations at `createAuth`. Password-reset request remains email-address-only; username-only accounts and `"either"` accounts created without an email cannot request an email reset. `emailPolicy` denies email-less users, including session creation through OIDC. No additional migration is needed for this identity-mode change: D1 rate-limit table keys store only digests, with identifier kind included in the hashed key.
179
+
180
+
181
+ ## Email verification and password reset
182
+
183
+ These flows are opt-in. Configure the matching options with an `AuthActionTokenStore` and a delivery callback. The store persists only a SHA-256 token hash, atomically replaces an earlier token for the same user and purpose, consumes the current unexpired token once, and implements bounded expiration cleanup. The callback receives the raw bearer token: enqueue it promptly and never log it. Submit tokens to the POST confirmation routes rather than consuming them in GET requests, which link scanners may follow.
184
+
185
+ Email verification also requires `isVerified` and `markVerified`. When enabled, unverified users cannot log in or use existing sessions. The configured package `passwordPolicy` applies to registration and password-reset confirmation. Reset sessions are revoked before `setPassword` delegates hashing and persistence to the application; if persistence fails, the user remains signed out but the old password may still work. The application owns hashing, persistence, and any policy checks for password flows outside these package handlers.
186
+
187
+ Known and unknown addresses receive the same request response, though delivery timing can differ. Use asynchronous delivery and configure rate limiting for public auth endpoints.
188
+
189
+ ## External identity handlers
190
+
191
+ `auth.createSessionResponse(request, userId)` creates a package session after an external identity handler has verified its protocol response. It reloads the application user and applies configured host, email, IP, and verification policies. The caller remains responsible for protocol state, CSRF protection, and rate limiting.
192
+
193
+ ## Security and storage contracts
194
+
195
+ - The default `__Host-shtora.session` cookie is `Secure`, `HttpOnly`, `SameSite=Lax`, `Path=/`, and has no `Domain`. Cookie names must use the `__Host-` prefix.
196
+ - Session cookies contain a random 256-bit token. `SessionStore` persists only its SHA-256 hash. Use storage with reliable revocation consistency; `deleteByIdForUser` must match both session and user IDs.
197
+ - The default session lifetime is seven days (maximum one year). Request bodies are limited to 8 KiB by default; oversized cookie headers are treated as unauthenticated.
198
+ - The application must hash passwords before storing them. `CredentialVerifier.verify` should do equivalent work for missing users to reduce account-enumeration timing differences. Never return password material through `AuthUser`.
199
+ - Rate limiting is optional for core routes and should be configured for public deployments. OIDC and WebAuthn require a trusted client-IP resolver and request rate limiter; they fail closed when the IP is unavailable.
200
+ - `resolveClientIp` must use a trusted proxy or platform header. Never trust arbitrary forwarded headers.
201
+ - `allowedHosts` is an optional exact-authority allowlist. If omitted, reject unexpected hosts at the platform or proxy.
202
+ - `onError({ operation, error })` observes unexpected adapter, policy, and delivery errors. HTTP responses remain generic. Avoid logging unreviewed adapter errors; they may contain sensitive values. Direct `getSession()` and `createSessionResponse()` calls reject adapter errors after notifying the observer.
203
+
204
+ The application also owns persistent storage for action tokens, OIDC transactions, WebAuthn challenges, and credentials. Their `consume` operations must be atomic and one-use; WebAuthn credential IDs must be unique and counter updates conditional on the stored counter. Expired-record cleanup methods must honor their supplied limit. See the exported TypeScript interfaces for the complete contracts.
205
+
206
+ ## Optional protocols
207
+
208
+ ### OIDC
209
+
210
+ Import `createOidc` and its types from `@shtora/auth/oidc`. Required options are `issuer` (HTTPS issuer URL), `clientId`, an absolute HTTPS `callbackUrl` registered with the provider, a fixed same-origin `successPath`, a persistent `transactions` store, `resolveUser`, `auth`, a trusted `resolveClientIp`, and a `rateLimiter`. `auth` is the `createAuth` result; `resolveUser` maps the verified `{ issuer, subject }` to an application user or `null`.
211
+
212
+ The returned handler accepts GET `/start` and GET `/callback` paths. Mount it at the callback URL's origin and route both paths to it; register the exact callback URL with the identity provider. It uses state, nonce, PKCE, and one-use transaction consumption. Schedule its `cleanupExpired()` method.
213
+
214
+ ### WebAuthn and passkeys
215
+
216
+ Import `createWebAuthn` and its types from `@shtora/auth/webauthn`. Required options are the exact `expectedOrigin` (HTTPS, or localhost HTTP for development), matching `rpID`, `rpName`, persistent one-use `challenges` and `credentials` stores, `resolveRegistrationUser`, `auth`, a trusted `resolveClientIp`, and a `rateLimiter`. `auth` is the `createAuth` result. Registration requires an authenticated application user with a stable, non-PII ID of at most 64 UTF-8 bytes.
217
+
218
+ Forward POST `/webauthn/registration/options`, `/webauthn/registration/verify`, `/webauthn/authentication/options`, and `/webauthn/authentication/verify` to the returned handler. It verifies the configured origin and RP ID, requires user verification, and uses one-use challenges. Credential IDs must be unique and counter updates atomic compare-and-set operations. Schedule the returned `cleanupExpired()` method.
219
+
220
+ ## Public entry points
221
+
222
+ - `@shtora/auth`: `createAuth` and the core types for users, credentials, sessions, rate limiting, policies, and optional email verification/password reset.
223
+ - `@shtora/auth/cloudflare`: `D1Database`, D1 session/action-token stores, core and request rate limiters, and D1 auth-state cleanup.
224
+ - `@shtora/auth/oidc`: `createOidc` and OIDC configuration, transaction-store, and handler types.
225
+ - `@shtora/auth/webauthn`: `createWebAuthn` and WebAuthn challenge-store, credential-store, options, and user types.
226
+
227
+ Install `openid-client` for `@shtora/auth/oidc` and `@simplewebauthn/server` for `@shtora/auth/webauthn`; these optional peer dependencies are not needed by the core entry point.
228
+
229
+ ## Cloudflare D1 adapter
230
+
231
+ `@shtora/auth/cloudflare` exports `createD1SessionStore`, `createD1ActionTokenStore`, `createD1RateLimiter`, `createD1RequestRateLimiter`, and `cleanupD1AuthState`. `createD1RateLimiter` implements the core auth limiter for registration, login, email verification, and password reset; `createD1RequestRateLimiter` implements the separate request limiter required by OIDC and WebAuthn using `{ limit, windowSeconds }`. Both use hashed keys in the same `shtora_rate_limits` table.
232
+
233
+ The npm package omits SQL files; apply migrations in filename order from the [pinned repository source](https://github.com/orgShtora-1/auth/tree/694089c260cdcc229b78b20a6b3a90f92eb38fd6/migrations), using `wrangler d1 execute <database-name> --remote --file <migration.sql>`. If schemas diverge in a later release, use the repository commit matching your installed package version. `cleanupD1AuthState` deletes bounded batches of expired sessions, action tokens, and rate-limit records; set `rateLimitMaxWindowSeconds` to at least the longest configured rate-limit window. D1 counters share one database and consume D1 write capacity. Cloudflare's Workers Rate Limit binding is location-local and approximate, not a globally strict limiter. See the [D1 Worker API](https://developers.cloudflare.com/d1/worker-api/d1-database/) and [Workers Rate Limit binding](https://developers.cloudflare.com/workers/runtime-apis/bindings/rate-limit/).
234
+
235
+
package/dist/auth.d.ts ADDED
@@ -0,0 +1,2 @@
1
+ import type { AuthApi, AuthOptions, AuthUser } from "./types.js";
2
+ export declare function createAuth<User extends AuthUser>(options: AuthOptions<User>): AuthApi;