@lastshotlabs/bunshot 0.0.9 → 0.0.13
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/README.md +241 -7
- package/dist/adapters/memoryAuth.d.ts +5 -0
- package/dist/adapters/memoryAuth.js +23 -0
- package/dist/adapters/sqliteAuth.d.ts +5 -0
- package/dist/adapters/sqliteAuth.js +18 -0
- package/dist/app.d.ts +48 -2
- package/dist/app.js +72 -5
- package/dist/entrypoints/mongo.d.ts +3 -0
- package/dist/entrypoints/mongo.js +3 -0
- package/dist/entrypoints/queue.d.ts +2 -0
- package/dist/entrypoints/queue.js +1 -0
- package/dist/entrypoints/redis.d.ts +1 -0
- package/dist/entrypoints/redis.js +1 -0
- package/dist/index.d.ts +5 -7
- package/dist/index.js +5 -5
- package/dist/lib/appConfig.d.ts +9 -0
- package/dist/lib/appConfig.js +5 -0
- package/dist/lib/createRoute.d.ts +61 -0
- package/dist/lib/createRoute.js +147 -0
- package/dist/lib/emailVerification.js +11 -10
- package/dist/lib/mongo.d.ts +9 -4
- package/dist/lib/mongo.js +61 -10
- package/dist/lib/oauth.js +11 -10
- package/dist/lib/queue.d.ts +3 -4
- package/dist/lib/queue.js +18 -3
- package/dist/lib/redis.d.ts +3 -8
- package/dist/lib/redis.js +19 -8
- package/dist/lib/resetPassword.d.ts +12 -0
- package/dist/lib/resetPassword.js +95 -0
- package/dist/lib/session.js +12 -12
- package/dist/middleware/cacheResponse.js +10 -9
- package/dist/models/AuthUser.d.ts +14 -106
- package/dist/models/AuthUser.js +31 -14
- package/dist/routes/auth.d.ts +3 -2
- package/dist/routes/auth.js +176 -59
- package/dist/services/auth.d.ts +8 -1
- package/dist/services/auth.js +5 -3
- package/package.json +38 -8
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
type ResetStore = "redis" | "mongo" | "sqlite" | "memory";
|
|
2
|
+
export declare const setPasswordResetStore: (store: ResetStore) => void;
|
|
3
|
+
/** Create a reset token. Returns the raw token (to embed in the email link).
|
|
4
|
+
* Only the SHA-256 hash is persisted in the store. */
|
|
5
|
+
export declare const createResetToken: (userId: string, email: string) => Promise<string>;
|
|
6
|
+
/** Atomically consume a reset token — returns its payload and deletes it in one operation.
|
|
7
|
+
* Returns null if the token is invalid, expired, or already used. */
|
|
8
|
+
export declare const consumeResetToken: (token: string) => Promise<{
|
|
9
|
+
userId: string;
|
|
10
|
+
email: string;
|
|
11
|
+
} | null>;
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import { createHash } from "crypto";
|
|
2
|
+
import { getRedis } from "./redis";
|
|
3
|
+
import { appConnection } from "./mongo";
|
|
4
|
+
import { getAppName, getResetTokenExpiry } from "./appConfig";
|
|
5
|
+
import { Schema } from "mongoose";
|
|
6
|
+
import { sqliteCreateResetToken, sqliteConsumeResetToken, } from "../adapters/sqliteAuth";
|
|
7
|
+
import { memoryCreateResetToken, memoryConsumeResetToken, } from "../adapters/memoryAuth";
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Token hashing — store SHA-256(token); raw token is only in the email link.
|
|
10
|
+
// If the store is ever leaked, outstanding tokens cannot be replayed directly.
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
const hashToken = (token) => createHash("sha256").update(token).digest("hex");
|
|
13
|
+
const resetSchema = new Schema({
|
|
14
|
+
token: { type: String, required: true, unique: true },
|
|
15
|
+
userId: { type: String, required: true },
|
|
16
|
+
email: { type: String, required: true },
|
|
17
|
+
expiresAt: { type: Date, required: true, index: { expireAfterSeconds: 0 } },
|
|
18
|
+
}, { collection: "password_resets" });
|
|
19
|
+
function getResetModel() {
|
|
20
|
+
return appConnection.models["PasswordReset"] ??
|
|
21
|
+
appConnection.model("PasswordReset", resetSchema);
|
|
22
|
+
}
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// Redis helpers
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
/** Atomically GET+DEL a key. Uses native GETDEL (Redis >= 6.2) with a Lua fallback. */
|
|
27
|
+
async function redisGetDel(key) {
|
|
28
|
+
const redis = getRedis();
|
|
29
|
+
if (typeof redis.getdel === "function") {
|
|
30
|
+
try {
|
|
31
|
+
return await redis.getdel(key);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
const msg = err?.message ?? "";
|
|
35
|
+
if (!/unknown command|ERR unknown command/i.test(msg))
|
|
36
|
+
throw err;
|
|
37
|
+
// Fall through to Lua on "unknown command"
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
const result = await redis.eval("local v = redis.call('GET', KEYS[1])\nif v then redis.call('DEL', KEYS[1]) end\nreturn v", 1, key);
|
|
41
|
+
return result ?? null;
|
|
42
|
+
}
|
|
43
|
+
let _store = "redis";
|
|
44
|
+
export const setPasswordResetStore = (store) => { _store = store; };
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// Public API
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
/** Create a reset token. Returns the raw token (to embed in the email link).
|
|
49
|
+
* Only the SHA-256 hash is persisted in the store. */
|
|
50
|
+
export const createResetToken = async (userId, email) => {
|
|
51
|
+
const token = crypto.randomUUID();
|
|
52
|
+
const hash = hashToken(token);
|
|
53
|
+
const ttl = getResetTokenExpiry();
|
|
54
|
+
if (_store === "memory") {
|
|
55
|
+
memoryCreateResetToken(hash, userId, email, ttl);
|
|
56
|
+
return token;
|
|
57
|
+
}
|
|
58
|
+
if (_store === "sqlite") {
|
|
59
|
+
sqliteCreateResetToken(hash, userId, email, ttl);
|
|
60
|
+
return token;
|
|
61
|
+
}
|
|
62
|
+
if (_store === "mongo") {
|
|
63
|
+
await getResetModel().create({
|
|
64
|
+
token: hash,
|
|
65
|
+
userId,
|
|
66
|
+
email,
|
|
67
|
+
expiresAt: new Date(Date.now() + ttl * 1000),
|
|
68
|
+
});
|
|
69
|
+
return token;
|
|
70
|
+
}
|
|
71
|
+
await getRedis().set(`reset:${getAppName()}:${hash}`, JSON.stringify({ userId, email }), "EX", ttl);
|
|
72
|
+
return token;
|
|
73
|
+
};
|
|
74
|
+
/** Atomically consume a reset token — returns its payload and deletes it in one operation.
|
|
75
|
+
* Returns null if the token is invalid, expired, or already used. */
|
|
76
|
+
export const consumeResetToken = async (token) => {
|
|
77
|
+
const hash = hashToken(token);
|
|
78
|
+
if (_store === "memory")
|
|
79
|
+
return memoryConsumeResetToken(hash);
|
|
80
|
+
if (_store === "sqlite")
|
|
81
|
+
return sqliteConsumeResetToken(hash);
|
|
82
|
+
if (_store === "mongo") {
|
|
83
|
+
const doc = await getResetModel()
|
|
84
|
+
.findOneAndDelete({ token: hash, expiresAt: { $gt: new Date() } })
|
|
85
|
+
.lean();
|
|
86
|
+
if (!doc)
|
|
87
|
+
return null;
|
|
88
|
+
return { userId: doc.userId, email: doc.email };
|
|
89
|
+
}
|
|
90
|
+
// Redis: atomically return and remove the key (GETDEL or Lua fallback)
|
|
91
|
+
const raw = await redisGetDel(`reset:${getAppName()}:${hash}`);
|
|
92
|
+
if (!raw)
|
|
93
|
+
return null;
|
|
94
|
+
return JSON.parse(raw);
|
|
95
|
+
};
|
package/dist/lib/session.js
CHANGED
|
@@ -1,22 +1,22 @@
|
|
|
1
1
|
import { getRedis } from "./redis";
|
|
2
|
-
import { appConnection } from "./mongo";
|
|
2
|
+
import { appConnection, mongoose } from "./mongo";
|
|
3
3
|
import { getAppName, getPersistSessionMetadata, getIncludeInactiveSessions } from "./appConfig";
|
|
4
|
-
import { Schema } from "mongoose";
|
|
5
4
|
import { sqliteCreateSession, sqliteGetSession, sqliteDeleteSession, sqliteGetUserSessions, sqliteGetActiveSessionCount, sqliteEvictOldestSession, sqliteUpdateSessionLastActive, } from "../adapters/sqliteAuth";
|
|
6
5
|
import { memoryCreateSession, memoryGetSession, memoryDeleteSession, memoryGetUserSessions, memoryGetActiveSessionCount, memoryEvictOldestSession, memoryUpdateSessionLastActive, } from "../adapters/memoryAuth";
|
|
7
|
-
const sessionSchema = new Schema({
|
|
8
|
-
sessionId: { type: String, required: true, unique: true },
|
|
9
|
-
userId: { type: String, required: true, index: true },
|
|
10
|
-
token: { type: String, default: null },
|
|
11
|
-
createdAt: { type: Date, required: true },
|
|
12
|
-
lastActiveAt: { type: Date, required: true },
|
|
13
|
-
expiresAt: { type: Date, required: true },
|
|
14
|
-
ipAddress: { type: String },
|
|
15
|
-
userAgent: { type: String },
|
|
16
|
-
}, { collection: "sessions", timestamps: false });
|
|
17
6
|
function getSessionModel() {
|
|
18
7
|
if (appConnection.models["Session"])
|
|
19
8
|
return appConnection.models["Session"];
|
|
9
|
+
const { Schema } = mongoose;
|
|
10
|
+
const sessionSchema = new Schema({
|
|
11
|
+
sessionId: { type: String, required: true, unique: true },
|
|
12
|
+
userId: { type: String, required: true, index: true },
|
|
13
|
+
token: { type: String, default: null },
|
|
14
|
+
createdAt: { type: Date, required: true },
|
|
15
|
+
lastActiveAt: { type: Date, required: true },
|
|
16
|
+
expiresAt: { type: Date, required: true },
|
|
17
|
+
ipAddress: { type: String },
|
|
18
|
+
userAgent: { type: String },
|
|
19
|
+
}, { collection: "sessions", timestamps: false });
|
|
20
20
|
// Add TTL index only when metadata is not persisted — docs auto-delete at expiresAt.
|
|
21
21
|
// When persisting, token is nulled (soft-delete) but the row is kept indefinitely.
|
|
22
22
|
if (!getPersistSessionMetadata()) {
|
|
@@ -1,17 +1,18 @@
|
|
|
1
1
|
import { getRedis } from "../lib/redis";
|
|
2
2
|
import { getAppName } from "../lib/appConfig";
|
|
3
|
-
import { appConnection } from "../lib/mongo";
|
|
4
|
-
import { Schema } from "mongoose";
|
|
3
|
+
import { appConnection, mongoose } from "../lib/mongo";
|
|
5
4
|
import { isSqliteReady, sqliteGetCache, sqliteSetCache, sqliteDelCache, sqliteDelCachePattern } from "../adapters/sqliteAuth";
|
|
6
5
|
import { memoryGetCache, memorySetCache, memoryDelCache, memoryDelCachePattern } from "../adapters/memoryAuth";
|
|
7
|
-
const cacheSchema = new Schema({
|
|
8
|
-
key: { type: String, required: true, unique: true },
|
|
9
|
-
value: { type: String, required: true },
|
|
10
|
-
expiresAt: { type: Date, index: { expireAfterSeconds: 0 } },
|
|
11
|
-
}, { collection: "cache_entries" });
|
|
12
6
|
function getCacheModel() {
|
|
13
|
-
|
|
14
|
-
appConnection.
|
|
7
|
+
if (appConnection.models["CacheEntry"])
|
|
8
|
+
return appConnection.models["CacheEntry"];
|
|
9
|
+
const { Schema } = mongoose;
|
|
10
|
+
const cacheSchema = new Schema({
|
|
11
|
+
key: { type: String, required: true, unique: true },
|
|
12
|
+
value: { type: String, required: true },
|
|
13
|
+
expiresAt: { type: Date, index: { expireAfterSeconds: 0 } },
|
|
14
|
+
}, { collection: "cache_entries" });
|
|
15
|
+
return appConnection.model("CacheEntry", cacheSchema);
|
|
15
16
|
}
|
|
16
17
|
function isMongoReady() {
|
|
17
18
|
return appConnection.readyState === 1;
|
|
@@ -1,112 +1,20 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
1
|
+
import type { Document, Model } from "mongoose";
|
|
2
|
+
interface IAuthUser {
|
|
3
|
+
email?: string | null;
|
|
4
|
+
password?: string | null;
|
|
5
|
+
/** Compound provider keys: ["google:123456", "apple:000111"] */
|
|
3
6
|
providerIds: string[];
|
|
4
|
-
|
|
7
|
+
/** App-defined roles assigned to this user: ["admin", "editor", ...] */
|
|
5
8
|
roles: string[];
|
|
6
|
-
email
|
|
7
|
-
password?: string | null | undefined;
|
|
8
|
-
} & mongoose.DefaultTimestampProps, {}, {}, {
|
|
9
|
-
id: string;
|
|
10
|
-
}, mongoose.Document<unknown, {}, {
|
|
11
|
-
providerIds: string[];
|
|
9
|
+
/** Whether the user's email address has been verified. */
|
|
12
10
|
emailVerified: boolean;
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
}, {
|
|
19
|
-
timestamps: true;
|
|
20
|
-
}> & Omit<{
|
|
21
|
-
providerIds: string[];
|
|
22
|
-
emailVerified: boolean;
|
|
23
|
-
roles: string[];
|
|
24
|
-
email?: string | null | undefined;
|
|
25
|
-
password?: string | null | undefined;
|
|
26
|
-
} & mongoose.DefaultTimestampProps & {
|
|
27
|
-
_id: mongoose.Types.ObjectId;
|
|
28
|
-
} & {
|
|
11
|
+
}
|
|
12
|
+
type AuthUserDocument = IAuthUser & Document;
|
|
13
|
+
export declare const AuthUser: Model<AuthUserDocument, {}, {}, {}, Document<unknown, {}, AuthUserDocument, {}, import("mongoose").DefaultSchemaOptions> & IAuthUser & Document<import("mongoose").Types.ObjectId, any, any, Record<string, any>, {}> & Required<{
|
|
14
|
+
_id: import("mongoose").Types.ObjectId;
|
|
15
|
+
}> & {
|
|
29
16
|
__v: number;
|
|
30
|
-
}, "id"> & {
|
|
31
|
-
id: string;
|
|
32
|
-
}, mongoose.Schema<any, mongoose.Model<any, any, any, any, any, any, any>, {}, {}, {}, {}, {
|
|
33
|
-
timestamps: true;
|
|
34
|
-
}, {
|
|
35
|
-
providerIds: string[];
|
|
36
|
-
emailVerified: boolean;
|
|
37
|
-
roles: string[];
|
|
38
|
-
email?: string | null | undefined;
|
|
39
|
-
password?: string | null | undefined;
|
|
40
|
-
} & mongoose.DefaultTimestampProps, mongoose.Document<unknown, {}, {
|
|
41
|
-
providerIds: string[];
|
|
42
|
-
emailVerified: boolean;
|
|
43
|
-
roles: string[];
|
|
44
|
-
email?: string | null | undefined;
|
|
45
|
-
password?: string | null | undefined;
|
|
46
|
-
} & mongoose.DefaultTimestampProps, {
|
|
47
|
-
id: string;
|
|
48
|
-
}, mongoose.MergeType<mongoose.DefaultSchemaOptions, {
|
|
49
|
-
timestamps: true;
|
|
50
|
-
}>> & Omit<{
|
|
51
|
-
providerIds: string[];
|
|
52
|
-
emailVerified: boolean;
|
|
53
|
-
roles: string[];
|
|
54
|
-
email?: string | null | undefined;
|
|
55
|
-
password?: string | null | undefined;
|
|
56
|
-
} & mongoose.DefaultTimestampProps & {
|
|
57
|
-
_id: mongoose.Types.ObjectId;
|
|
58
17
|
} & {
|
|
59
|
-
__v: number;
|
|
60
|
-
}, "id"> & {
|
|
61
18
|
id: string;
|
|
62
|
-
},
|
|
63
|
-
|
|
64
|
-
} | {
|
|
65
|
-
[x: string]: mongoose.SchemaDefinitionProperty<any, any, mongoose.Document<unknown, {}, {
|
|
66
|
-
providerIds: string[];
|
|
67
|
-
emailVerified: boolean;
|
|
68
|
-
roles: string[];
|
|
69
|
-
email?: string | null | undefined;
|
|
70
|
-
password?: string | null | undefined;
|
|
71
|
-
} & mongoose.DefaultTimestampProps, {
|
|
72
|
-
id: string;
|
|
73
|
-
}, mongoose.MergeType<mongoose.DefaultSchemaOptions, {
|
|
74
|
-
timestamps: true;
|
|
75
|
-
}>> & Omit<{
|
|
76
|
-
providerIds: string[];
|
|
77
|
-
emailVerified: boolean;
|
|
78
|
-
roles: string[];
|
|
79
|
-
email?: string | null | undefined;
|
|
80
|
-
password?: string | null | undefined;
|
|
81
|
-
} & mongoose.DefaultTimestampProps & {
|
|
82
|
-
_id: mongoose.Types.ObjectId;
|
|
83
|
-
} & {
|
|
84
|
-
__v: number;
|
|
85
|
-
}, "id"> & {
|
|
86
|
-
id: string;
|
|
87
|
-
}> | undefined;
|
|
88
|
-
}, {
|
|
89
|
-
providerIds: string[];
|
|
90
|
-
emailVerified: boolean;
|
|
91
|
-
roles: string[];
|
|
92
|
-
email?: string | null | undefined;
|
|
93
|
-
password?: string | null | undefined;
|
|
94
|
-
createdAt: NativeDate;
|
|
95
|
-
updatedAt: NativeDate;
|
|
96
|
-
} & {
|
|
97
|
-
_id: mongoose.Types.ObjectId;
|
|
98
|
-
} & {
|
|
99
|
-
__v: number;
|
|
100
|
-
}>, {
|
|
101
|
-
providerIds: string[];
|
|
102
|
-
emailVerified: boolean;
|
|
103
|
-
roles: string[];
|
|
104
|
-
email?: string | null | undefined;
|
|
105
|
-
password?: string | null | undefined;
|
|
106
|
-
createdAt: NativeDate;
|
|
107
|
-
updatedAt: NativeDate;
|
|
108
|
-
} & {
|
|
109
|
-
_id: mongoose.Types.ObjectId;
|
|
110
|
-
} & {
|
|
111
|
-
__v: number;
|
|
112
|
-
}>;
|
|
19
|
+
}, any, AuthUserDocument>;
|
|
20
|
+
export {};
|
package/dist/models/AuthUser.js
CHANGED
|
@@ -1,14 +1,31 @@
|
|
|
1
|
-
import mongoose from "
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
1
|
+
import { authConnection, mongoose } from "../lib/mongo";
|
|
2
|
+
// Lazily register the model — authConnection and mongoose are proxies that
|
|
3
|
+
// resolve once connectAuthMongo() / connectMongo() has been called.
|
|
4
|
+
let _AuthUser = null;
|
|
5
|
+
function getAuthUser() {
|
|
6
|
+
if (!_AuthUser) {
|
|
7
|
+
const { Schema } = mongoose;
|
|
8
|
+
const schema = new Schema({
|
|
9
|
+
email: { type: String, unique: true, sparse: true, lowercase: true },
|
|
10
|
+
password: { type: String },
|
|
11
|
+
/** Compound provider keys: ["google:123456", "apple:000111"] */
|
|
12
|
+
providerIds: [{ type: String }],
|
|
13
|
+
/** App-defined roles assigned to this user: ["admin", "editor", ...] */
|
|
14
|
+
roles: [{ type: String }],
|
|
15
|
+
/** Whether the user's email address has been verified. */
|
|
16
|
+
emailVerified: { type: Boolean, default: false },
|
|
17
|
+
}, { timestamps: true });
|
|
18
|
+
schema.index({ providerIds: 1 });
|
|
19
|
+
_AuthUser = authConnection.model("AuthUser", schema);
|
|
20
|
+
}
|
|
21
|
+
return _AuthUser;
|
|
22
|
+
}
|
|
23
|
+
// Export a Proxy so callers can use AuthUser.findOne() etc. at any time after
|
|
24
|
+
// connectAuthMongo() / connectMongo() has been called.
|
|
25
|
+
export const AuthUser = new Proxy({}, {
|
|
26
|
+
get(_, prop) {
|
|
27
|
+
const model = getAuthUser();
|
|
28
|
+
const val = model[prop];
|
|
29
|
+
return typeof val === "function" ? val.bind(model) : val;
|
|
30
|
+
},
|
|
31
|
+
});
|
package/dist/routes/auth.d.ts
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
|
-
import type { PrimaryField, EmailVerificationConfig } from "../lib/appConfig";
|
|
1
|
+
import type { PrimaryField, EmailVerificationConfig, PasswordResetConfig } from "../lib/appConfig";
|
|
2
2
|
import type { AuthRateLimitConfig } from "../app";
|
|
3
3
|
export interface AuthRouterOptions {
|
|
4
4
|
primaryField: PrimaryField;
|
|
5
5
|
emailVerification?: EmailVerificationConfig;
|
|
6
|
+
passwordReset?: PasswordResetConfig;
|
|
6
7
|
rateLimit?: AuthRateLimitConfig;
|
|
7
8
|
}
|
|
8
|
-
export declare const createAuthRouter: ({ primaryField, emailVerification, rateLimit }: AuthRouterOptions) => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;
|
|
9
|
+
export declare const createAuthRouter: ({ primaryField, emailVerification, passwordReset, rateLimit }: AuthRouterOptions) => import("@hono/zod-openapi").OpenAPIHono<import("../lib/context").AppEnv, {}, "/">;
|