@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.
@@ -0,0 +1,173 @@
1
+ export interface AuthUser {
2
+ readonly id: string;
3
+ readonly email?: string;
4
+ readonly username?: string;
5
+ }
6
+ export type AuthIdentifier = {
7
+ readonly type: "email";
8
+ readonly value: string;
9
+ } | {
10
+ readonly type: "username";
11
+ readonly value: string;
12
+ };
13
+ export type RegistrationIdentityMode = "email" | "username" | "both" | "either";
14
+ export type LoginIdentityMode = "email" | "username" | "either";
15
+ export interface PasswordPolicy {
16
+ minLength?: number;
17
+ maxLength?: number;
18
+ requireUppercase?: boolean;
19
+ requireLowercase?: boolean;
20
+ requireSpecial?: boolean;
21
+ }
22
+ export interface UserStore<User extends AuthUser = AuthUser> {
23
+ findByIdentifier(identifier: AuthIdentifier): Promise<User | null>;
24
+ findByEmail(email: string): Promise<User | null>;
25
+ findById(id: string): Promise<User | null>;
26
+ /** Enforce canonical email/username uniqueness atomically; hash the password before persistence. */
27
+ create(input: {
28
+ email?: string;
29
+ username?: string;
30
+ password: string;
31
+ }): Promise<{
32
+ created: true;
33
+ user: User;
34
+ } | {
35
+ created: false;
36
+ }>;
37
+ }
38
+ export interface CredentialVerifier<User extends AuthUser = AuthUser> {
39
+ verify(input: {
40
+ identifier: AuthIdentifier;
41
+ password: string;
42
+ user: User | null;
43
+ }): Promise<boolean>;
44
+ }
45
+ export type AuthActionTokenPurpose = "email-verification" | "password-reset";
46
+ export interface AuthActionTokenStore extends ExpiredRecordStore {
47
+ /** Atomically replace any existing token for this user and purpose. */
48
+ create(input: {
49
+ purpose: AuthActionTokenPurpose;
50
+ userId: string;
51
+ tokenHash: string;
52
+ expiresAtMs: number;
53
+ }): Promise<void>;
54
+ /** Consume only an unexpired matching token, atomically; never return its stored hash. */
55
+ consume(input: {
56
+ purpose: AuthActionTokenPurpose;
57
+ tokenHash: string;
58
+ nowMs: number;
59
+ }): Promise<{
60
+ userId: string;
61
+ } | null>;
62
+ }
63
+ export interface AuthActionTokenDelivery {
64
+ /** Enqueue delivery promptly; callers must not log the bearer token. */
65
+ send(input: {
66
+ email: string;
67
+ token: string;
68
+ expiresAtMs: number;
69
+ }): Promise<void>;
70
+ tokenTtlSeconds?: number;
71
+ }
72
+ export interface EmailVerificationOptions<User extends AuthUser = AuthUser> extends AuthActionTokenDelivery {
73
+ tokens: AuthActionTokenStore;
74
+ isVerified(user: User): boolean;
75
+ markVerified(userId: string): Promise<void>;
76
+ }
77
+ export interface PasswordResetOptions extends AuthActionTokenDelivery {
78
+ tokens: AuthActionTokenStore;
79
+ /** Update the application's password hash; this package never hashes or stores passwords. */
80
+ setPassword(input: {
81
+ userId: string;
82
+ password: string;
83
+ }): Promise<void>;
84
+ }
85
+ /** id is random and non-secret; tokenHash is the cookie token digest. Timestamps are Unix epoch milliseconds. */
86
+ export interface StoredSession {
87
+ readonly id: string;
88
+ readonly tokenHash: string;
89
+ readonly userId: string;
90
+ readonly createdAtMs: number;
91
+ readonly expiresAtMs: number;
92
+ }
93
+ export interface AuthOptions<User extends AuthUser = AuthUser> {
94
+ users: UserStore<User>;
95
+ credentials: CredentialVerifier<User>;
96
+ sessions: SessionStore;
97
+ identity?: {
98
+ registration?: RegistrationIdentityMode;
99
+ login?: LoginIdentityMode;
100
+ };
101
+ passwordPolicy?: PasswordPolicy;
102
+ emailPolicy?: AccessPolicy;
103
+ resolveClientIp?: (request: Request) => string | null | Promise<string | null>;
104
+ ipPolicy?: AccessPolicy;
105
+ rateLimiter?: AuthRateLimiter;
106
+ emailVerification?: EmailVerificationOptions<User>;
107
+ passwordReset?: PasswordResetOptions;
108
+ allowedHosts?: readonly string[];
109
+ onError?: (event: {
110
+ operation: string;
111
+ error: unknown;
112
+ }) => void | Promise<void>;
113
+ requireOrigin?: boolean;
114
+ basePath?: string;
115
+ cookieName?: string;
116
+ sessionTtlSeconds?: number;
117
+ maxRequestBytes?: number;
118
+ }
119
+ export interface ExpiredRecordStore {
120
+ /** Delete at most `limit` expired records; call periodically from a scheduled task. */
121
+ deleteExpired(input: {
122
+ nowMs: number;
123
+ limit: number;
124
+ }): Promise<void>;
125
+ }
126
+ export interface SessionStore extends ExpiredRecordStore {
127
+ create(session: StoredSession): Promise<void>;
128
+ findByTokenHash(tokenHash: string): Promise<StoredSession | null>;
129
+ deleteByTokenHash(tokenHash: string): Promise<void>;
130
+ listByUserId(userId: string): Promise<readonly StoredSession[]>;
131
+ deleteByIdForUser(id: string, userId: string): Promise<void>;
132
+ deleteByUserId(userId: string): Promise<void>;
133
+ }
134
+ export interface AuthRateLimiter {
135
+ check(input: {
136
+ action: "register" | "login" | "email-verification" | "password-reset";
137
+ identifier: AuthIdentifier;
138
+ ip: string;
139
+ }): Promise<{
140
+ allowed: boolean;
141
+ retryAfterSeconds?: number;
142
+ }>;
143
+ }
144
+ export type AuthRequestRateLimitAction = "oidc-start" | "oidc-callback" | "webauthn-registration-options" | "webauthn-registration-verify" | "webauthn-authentication-options" | "webauthn-authentication-verify";
145
+ export interface AuthRequestRateLimiter {
146
+ check(input: {
147
+ action: AuthRequestRateLimitAction;
148
+ ip: string;
149
+ }): Promise<{
150
+ allowed: boolean;
151
+ retryAfterSeconds?: number;
152
+ }>;
153
+ }
154
+ export interface AccessPolicy {
155
+ allow(input: {
156
+ value: string;
157
+ request: Request;
158
+ }): Promise<boolean>;
159
+ }
160
+ export interface AuthenticatedUser {
161
+ readonly id: string;
162
+ readonly email?: string;
163
+ readonly username?: string;
164
+ }
165
+ export interface AuthApi {
166
+ handle(request: Request): Promise<Response>;
167
+ /** Delete a bounded batch of expired sessions and action tokens from each configured store. */
168
+ cleanupExpired(): Promise<void>;
169
+ /** Rejects if a backing adapter fails; unexpected errors are observed before being rethrown. */
170
+ getSession(request: Request): Promise<AuthenticatedUser | null>;
171
+ /** Use only after an external identity protocol has been verified; creates a server-side session. */
172
+ createSessionResponse(request: Request, userId: string): Promise<Response>;
173
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,68 @@
1
+ import type { WebAuthnCredential } from "@simplewebauthn/server";
2
+ import type { AuthApi, AuthRequestRateLimiter, ExpiredRecordStore } from "./types.js";
3
+ export type WebAuthnChallengePurpose = "registration" | "authentication";
4
+ /** Stores must consume atomically and delete at most the requested number of expired challenges per cleanup call. */
5
+ export interface WebAuthnChallengeStore extends ExpiredRecordStore {
6
+ create(challenge: {
7
+ id: string;
8
+ purpose: WebAuthnChallengePurpose;
9
+ userId: string | null;
10
+ value: string;
11
+ expiresAtMs: number;
12
+ }): Promise<void>;
13
+ consume(input: {
14
+ id: string;
15
+ purpose: WebAuthnChallengePurpose;
16
+ userId: string | null;
17
+ nowMs: number;
18
+ }): Promise<{
19
+ value: string;
20
+ } | null>;
21
+ }
22
+ /** The counter update must be atomic compare-and-set; create must enforce credential-ID uniqueness. */
23
+ export interface WebAuthnCredentialStore {
24
+ listByUserId(userId: string): Promise<readonly WebAuthnCredential[]>;
25
+ findById(id: string): Promise<{
26
+ userId: string;
27
+ credential: WebAuthnCredential;
28
+ } | null>;
29
+ create(input: {
30
+ userId: string;
31
+ credential: WebAuthnCredential;
32
+ }): Promise<boolean>;
33
+ updateCounter(input: {
34
+ id: string;
35
+ expectedCounter: number;
36
+ newCounter: number;
37
+ }): Promise<boolean>;
38
+ }
39
+ export interface WebAuthnUser {
40
+ /** Stable opaque application ID, encoded as UTF-8 and limited to 64 bytes. */
41
+ id: string;
42
+ name: string;
43
+ displayName: string;
44
+ }
45
+ export interface WebAuthnOptions {
46
+ /** Exact origin, including scheme and port; never inferred from an incoming request. */
47
+ expectedOrigin: string;
48
+ /** Exact RP ID configured by the relying party. */
49
+ rpID: string;
50
+ rpName: string;
51
+ challenges: WebAuthnChallengeStore;
52
+ credentials: WebAuthnCredentialStore;
53
+ /** Return the authenticated application user for registration, or null. */
54
+ resolveRegistrationUser(request: Request): Promise<WebAuthnUser | null>;
55
+ auth: Pick<AuthApi, "createSessionResponse">;
56
+ resolveClientIp: (request: Request) => string | null | Promise<string | null>;
57
+ rateLimiter: AuthRequestRateLimiter;
58
+ challengeTtlMs?: number;
59
+ onError?(input: {
60
+ operation: string;
61
+ error: unknown;
62
+ }): void | Promise<void>;
63
+ }
64
+ /** Creates framework-neutral WebAuthn registration and authentication HTTP handlers. */
65
+ export declare function createWebAuthn(options: WebAuthnOptions): {
66
+ handle(request: Request): Promise<Response>;
67
+ cleanupExpired(): Promise<void>;
68
+ };
@@ -0,0 +1,302 @@
1
+ import { generateAuthenticationOptions, generateRegistrationOptions, verifyAuthenticationResponse, verifyRegistrationResponse, } from "@simplewebauthn/server";
2
+ const defaultChallengeTtlMs = 5 * 60 * 1000;
3
+ const maxChallengeTtlMs = 15 * 60 * 1000;
4
+ const maxBodyBytes = 64 * 1024;
5
+ function json(body, status = 200, headers) {
6
+ return new Response(JSON.stringify(body), {
7
+ status,
8
+ headers: {
9
+ "Cache-Control": "no-store",
10
+ "Content-Type": "application/json; charset=utf-8",
11
+ "X-Content-Type-Options": "nosniff",
12
+ ...Object.fromEntries(new Headers(headers)),
13
+ },
14
+ });
15
+ }
16
+ function reject() {
17
+ return json({ error: "invalid_request" }, 400);
18
+ }
19
+ function randomId() {
20
+ const bytes = new Uint8Array(32);
21
+ crypto.getRandomValues(bytes);
22
+ let binary = "";
23
+ for (const byte of bytes)
24
+ binary += String.fromCharCode(byte);
25
+ return btoa(binary).replaceAll("+", "-").replaceAll("/", "_").replace(/=+$/, "");
26
+ }
27
+ function hasSameOrigin(request, expectedOrigin) {
28
+ const origin = request.headers.get("origin");
29
+ if (!origin)
30
+ return false;
31
+ try {
32
+ const parsed = new URL(origin);
33
+ return parsed.origin === origin &&
34
+ !parsed.username &&
35
+ !parsed.password &&
36
+ parsed.pathname === "/" &&
37
+ !parsed.search &&
38
+ !parsed.hash &&
39
+ origin === expectedOrigin;
40
+ }
41
+ catch {
42
+ return false;
43
+ }
44
+ }
45
+ async function readJson(request) {
46
+ const contentLength = Number(request.headers.get("content-length"));
47
+ if (Number.isFinite(contentLength) && contentLength > maxBodyBytes)
48
+ return null;
49
+ const reader = request.body?.getReader();
50
+ if (!reader)
51
+ return null;
52
+ const chunks = [];
53
+ let length = 0;
54
+ try {
55
+ while (true) {
56
+ const { done, value } = await reader.read();
57
+ if (done)
58
+ break;
59
+ length += value.byteLength;
60
+ if (length > maxBodyBytes) {
61
+ await reader.cancel();
62
+ return null;
63
+ }
64
+ chunks.push(value);
65
+ }
66
+ const bytes = new Uint8Array(length);
67
+ let offset = 0;
68
+ for (const chunk of chunks) {
69
+ bytes.set(chunk, offset);
70
+ offset += chunk.byteLength;
71
+ }
72
+ const value = JSON.parse(new TextDecoder("utf-8", { fatal: true }).decode(bytes));
73
+ return value !== null && typeof value === "object" && !Array.isArray(value)
74
+ ? value
75
+ : null;
76
+ }
77
+ catch {
78
+ return null;
79
+ }
80
+ finally {
81
+ reader.releaseLock();
82
+ }
83
+ }
84
+ function isLoopback(hostname) {
85
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
86
+ return host === "localhost" || host.endsWith(".localhost") || host === "::1" || host.startsWith("127.");
87
+ }
88
+ async function report(options, operation, error) {
89
+ try {
90
+ await options.onError?.({ operation, error });
91
+ }
92
+ catch {
93
+ // Error observers must not affect the generic public result.
94
+ }
95
+ }
96
+ /** Creates framework-neutral WebAuthn registration and authentication HTTP handlers. */
97
+ export function createWebAuthn(options) {
98
+ let expectedOrigin;
99
+ try {
100
+ const parsed = new URL(options.expectedOrigin);
101
+ const isSecureScheme = parsed.protocol === "https:";
102
+ const isLocalHttp = parsed.protocol === "http:" && isLoopback(parsed.hostname);
103
+ if (parsed.origin !== options.expectedOrigin || (!isSecureScheme && !isLocalHttp))
104
+ throw new TypeError();
105
+ const hostname = parsed.hostname.toLowerCase();
106
+ const rpID = options.rpID.toLowerCase();
107
+ if (parsed.origin !== options.expectedOrigin ||
108
+ (!isSecureScheme && !isLocalHttp) ||
109
+ !rpID ||
110
+ (hostname !== rpID && !hostname.endsWith(`.${rpID}`)) ||
111
+ !options.rpName.trim())
112
+ throw new TypeError();
113
+ expectedOrigin = parsed.origin;
114
+ }
115
+ catch {
116
+ throw new TypeError("expectedOrigin must be HTTPS (or localhost HTTP), rpID must match its host, and rpName must be non-empty");
117
+ }
118
+ const challengeTtlMs = options.challengeTtlMs ?? defaultChallengeTtlMs;
119
+ if (!Number.isSafeInteger(challengeTtlMs) || challengeTtlMs < 1_000 || challengeTtlMs > maxChallengeTtlMs) {
120
+ throw new RangeError("challengeTtlMs must be between 1000 and 900000");
121
+ }
122
+ async function createChallenge(purpose, userId, value) {
123
+ const id = randomId();
124
+ await options.challenges.create({
125
+ id,
126
+ purpose,
127
+ userId,
128
+ value,
129
+ expiresAtMs: Date.now() + challengeTtlMs,
130
+ });
131
+ return id;
132
+ }
133
+ async function registrationOptions(request) {
134
+ const user = await options.resolveRegistrationUser(request);
135
+ if (!user || !user.id || !user.name || !user.displayName)
136
+ return json({ error: "unauthenticated" }, 401);
137
+ const userID = new TextEncoder().encode(user.id);
138
+ if (userID.byteLength > 64)
139
+ throw new RangeError("WebAuthn user IDs must not exceed 64 UTF-8 bytes");
140
+ const existing = await options.credentials.listByUserId(user.id);
141
+ const publicKey = await generateRegistrationOptions({
142
+ rpName: options.rpName,
143
+ rpID: options.rpID,
144
+ userID,
145
+ userName: user.name,
146
+ userDisplayName: user.displayName,
147
+ authenticatorSelection: { residentKey: "required", userVerification: "required" },
148
+ excludeCredentials: existing.map(({ id, transports }) => ({ id, transports })),
149
+ });
150
+ const transactionId = await createChallenge("registration", user.id, publicKey.challenge);
151
+ return json({ transactionId, options: publicKey });
152
+ }
153
+ async function registrationVerify(request) {
154
+ const body = await readJson(request);
155
+ if (!body || typeof body.transactionId !== "string" || !body.response || typeof body.response !== "object")
156
+ return reject();
157
+ const user = await options.resolveRegistrationUser(request);
158
+ if (!user)
159
+ return json({ error: "unauthenticated" }, 401);
160
+ const transaction = await options.challenges.consume({
161
+ id: body.transactionId,
162
+ purpose: "registration",
163
+ userId: user.id,
164
+ nowMs: Date.now(),
165
+ });
166
+ if (!transaction)
167
+ return reject();
168
+ let verification;
169
+ try {
170
+ verification = await verifyRegistrationResponse({
171
+ response: body.response,
172
+ expectedChallenge: transaction.value,
173
+ expectedOrigin,
174
+ expectedRPID: options.rpID,
175
+ requireUserVerification: true,
176
+ });
177
+ }
178
+ catch {
179
+ return reject();
180
+ }
181
+ if (!verification.verified || !verification.registrationInfo)
182
+ return reject();
183
+ const { credential } = verification.registrationInfo;
184
+ if (!(await options.credentials.create({ userId: user.id, credential })))
185
+ return reject();
186
+ return json({ verified: true });
187
+ }
188
+ async function authenticationOptions() {
189
+ const publicKey = await generateAuthenticationOptions({
190
+ rpID: options.rpID,
191
+ userVerification: "required",
192
+ });
193
+ const transactionId = await createChallenge("authentication", null, publicKey.challenge);
194
+ return json({ transactionId, options: publicKey });
195
+ }
196
+ async function authenticationVerify(request) {
197
+ const body = await readJson(request);
198
+ if (!body || typeof body.transactionId !== "string" || !body.response || typeof body.response !== "object" ||
199
+ typeof body.response.id !== "string")
200
+ return reject();
201
+ const response = body.response;
202
+ const transaction = await options.challenges.consume({
203
+ id: body.transactionId,
204
+ purpose: "authentication",
205
+ userId: null,
206
+ nowMs: Date.now(),
207
+ });
208
+ if (!transaction)
209
+ return reject();
210
+ const stored = await options.credentials.findById(response.id);
211
+ if (!stored || stored.credential.id !== response.id)
212
+ return reject();
213
+ let verification;
214
+ try {
215
+ verification = await verifyAuthenticationResponse({
216
+ response,
217
+ expectedChallenge: transaction.value,
218
+ expectedOrigin,
219
+ expectedRPID: options.rpID,
220
+ credential: stored.credential,
221
+ requireUserVerification: true,
222
+ });
223
+ }
224
+ catch {
225
+ return reject();
226
+ }
227
+ if (!verification.verified)
228
+ return reject();
229
+ const updated = await options.credentials.updateCounter({
230
+ id: stored.credential.id,
231
+ expectedCounter: stored.credential.counter,
232
+ newCounter: verification.authenticationInfo.newCounter,
233
+ });
234
+ if (!updated)
235
+ return reject();
236
+ const session = await options.auth.createSessionResponse(request, stored.userId);
237
+ if (session.status === 200)
238
+ return session;
239
+ return json({ error: "unauthenticated" }, session.status === 403 ? 403 : 401);
240
+ }
241
+ async function handle(request) {
242
+ if (request.method !== "POST")
243
+ return json({ error: "method_not_allowed" }, 405);
244
+ if (!hasSameOrigin(request, expectedOrigin))
245
+ return json({ error: "invalid_origin" }, 403);
246
+ const path = new URL(request.url).pathname;
247
+ const routes = {
248
+ "/webauthn/registration/options": {
249
+ operation: "registration.options",
250
+ rateLimitAction: "webauthn-registration-options",
251
+ run: () => registrationOptions(request),
252
+ },
253
+ "/webauthn/registration/verify": {
254
+ operation: "registration.verify",
255
+ rateLimitAction: "webauthn-registration-verify",
256
+ run: () => registrationVerify(request),
257
+ },
258
+ "/webauthn/authentication/options": {
259
+ operation: "authentication.options",
260
+ rateLimitAction: "webauthn-authentication-options",
261
+ run: authenticationOptions,
262
+ },
263
+ "/webauthn/authentication/verify": {
264
+ operation: "authentication.verify",
265
+ rateLimitAction: "webauthn-authentication-verify",
266
+ run: () => authenticationVerify(request),
267
+ },
268
+ };
269
+ const route = routes[path];
270
+ if (!route)
271
+ return json({ error: "not_found" }, 404);
272
+ try {
273
+ const ip = await options.resolveClientIp(request);
274
+ const decision = ip
275
+ ? await options.rateLimiter.check({ action: route.rateLimitAction, ip })
276
+ : { allowed: false, retryAfterSeconds: 60 };
277
+ if (!decision.allowed) {
278
+ const seconds = Number.isSafeInteger(decision.retryAfterSeconds) && decision.retryAfterSeconds > 0
279
+ ? decision.retryAfterSeconds
280
+ : 60;
281
+ return json({ error: "rate_limited" }, 429, { "Retry-After": String(seconds) });
282
+ }
283
+ return await route.run();
284
+ }
285
+ catch (error) {
286
+ await report(options, route.operation, error);
287
+ return json({ error: "internal_error" }, 500);
288
+ }
289
+ }
290
+ return {
291
+ handle,
292
+ async cleanupExpired() {
293
+ try {
294
+ await options.challenges.deleteExpired({ nowMs: Date.now(), limit: 500 });
295
+ }
296
+ catch (error) {
297
+ await report(options, "challenge.cleanup", error);
298
+ throw error;
299
+ }
300
+ },
301
+ };
302
+ }
package/package.json CHANGED
@@ -1,6 +1,82 @@
1
1
  {
2
2
  "name": "@shtora/auth",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "description": "Framework-neutral auth handlers and session management for Next.js and Cloudflare Workers, with optional D1, OIDC, and WebAuthn adapters.",
4
+ "keywords": [
5
+ "authentication",
6
+ "auth",
7
+ "nextjs",
8
+ "next.js",
9
+ "cloudflare-workers",
10
+ "cloudflare-d1",
11
+ "workers",
12
+ "session-management",
13
+ "sessions",
14
+ "typescript",
15
+ "edge-runtime",
16
+ "oidc",
17
+ "openid-connect",
18
+ "webauthn",
19
+ "passkeys",
20
+ "email-verification",
21
+ "password-reset"
22
+ ],
23
+ "version": "0.1.1",
24
+ "type": "module",
25
+ "exports": {
26
+ ".": {
27
+ "types": "./dist/index.d.ts",
28
+ "import": "./dist/index.js"
29
+ },
30
+ "./cloudflare": {
31
+ "types": "./dist/cloudflare.d.ts",
32
+ "import": "./dist/cloudflare.js"
33
+ },
34
+ "./oidc": {
35
+ "types": "./dist/oidc.d.ts",
36
+ "import": "./dist/oidc.js"
37
+ },
38
+ "./webauthn": {
39
+ "types": "./dist/webauthn.d.ts",
40
+ "import": "./dist/webauthn.js"
41
+ }
42
+ },
43
+ "repository": {
44
+ "type": "git",
45
+ "url": "git+https://github.com/orgShtora-1/auth.git"
46
+ },
47
+ "homepage": "https://github.com/orgShtora-1/auth#readme",
48
+ "bugs": {
49
+ "url": "https://github.com/orgShtora-1/auth/issues"
50
+ },
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "peerDependencies": {
55
+ "@simplewebauthn/server": ">=14.0.3 <15",
56
+ "openid-client": ">=6 <7"
57
+ },
58
+ "peerDependenciesMeta": {
59
+ "@simplewebauthn/server": {
60
+ "optional": true
61
+ },
62
+ "openid-client": {
63
+ "optional": true
64
+ }
65
+ },
66
+ "files": [
67
+ "dist"
68
+ ],
69
+ "sideEffects": false,
70
+ "scripts": {
71
+ "build": "tsc -p tsconfig.build.json",
72
+ "prepack": "tsc -p tsconfig.build.json",
73
+ "typecheck": "tsc --noEmit",
74
+ "test": "bun test"
75
+ },
76
+ "devDependencies": {
77
+ "@simplewebauthn/server": "^14.0.3",
78
+ "@types/bun": "latest",
79
+ "openid-client": "^6.8.1",
80
+ "typescript": "^7.0.2"
81
+ }
82
+ }