@digiko-npm/cms 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.
- package/dist/auth/index.d.ts +20 -0
- package/dist/auth/index.js +34 -0
- package/dist/auth-C8Nq_GmD.d.ts +31 -0
- package/dist/config-qNdTlg1g.d.ts +64 -0
- package/dist/http/index.d.ts +15 -0
- package/dist/http/index.js +16 -0
- package/dist/index.d.ts +10 -0
- package/dist/index.js +233 -0
- package/dist/media-ExBfXePZ.d.ts +19 -0
- package/dist/next/index.d.ts +19 -0
- package/dist/next/index.js +54 -0
- package/dist/r2/index.d.ts +34 -0
- package/dist/r2/index.js +82 -0
- package/dist/session/index.d.ts +27 -0
- package/dist/session/index.js +83 -0
- package/dist/supabase/index.d.ts +21 -0
- package/dist/supabase/index.js +22 -0
- package/dist/types/index.d.ts +3 -0
- package/dist/types/index.js +0 -0
- package/package.json +101 -0
- package/src/auth/index.ts +2 -0
- package/src/auth/password.ts +48 -0
- package/src/auth/token.ts +11 -0
- package/src/http/index.ts +1 -0
- package/src/http/status.ts +14 -0
- package/src/index.ts +42 -0
- package/src/next/index.ts +1 -0
- package/src/next/verify-request.ts +54 -0
- package/src/r2/client.ts +28 -0
- package/src/r2/index.ts +2 -0
- package/src/r2/upload.ts +99 -0
- package/src/session/index.ts +2 -0
- package/src/session/rate-limit.ts +66 -0
- package/src/session/store.ts +56 -0
- package/src/supabase/client.ts +10 -0
- package/src/supabase/index.ts +2 -0
- package/src/supabase/server.ts +21 -0
- package/src/types/auth.ts +27 -0
- package/src/types/config.ts +65 -0
- package/src/types/index.ts +22 -0
- package/src/types/media.ts +19 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { A as AuthConfig } from '../config-qNdTlg1g.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Hash a password using PBKDF2.
|
|
5
|
+
* Returns a hex-encoded hash string.
|
|
6
|
+
*/
|
|
7
|
+
declare function hashPassword(password: string, salt: string, config?: AuthConfig['pbkdf2']): string;
|
|
8
|
+
/**
|
|
9
|
+
* Verify a password against a stored hash using timing-safe comparison.
|
|
10
|
+
* Returns true if the password matches.
|
|
11
|
+
*/
|
|
12
|
+
declare function verifyPassword(password: string, salt: string, storedHash: string, config?: AuthConfig['pbkdf2']): boolean;
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Generate a cryptographically secure session token.
|
|
16
|
+
* Returns a hex string (default: 64 characters from 32 bytes).
|
|
17
|
+
*/
|
|
18
|
+
declare function generateSessionToken(bytes?: number): string;
|
|
19
|
+
|
|
20
|
+
export { generateSessionToken, hashPassword, verifyPassword };
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// src/auth/password.ts
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
var DEFAULTS = {
|
|
4
|
+
iterations: 1e5,
|
|
5
|
+
keyLength: 64,
|
|
6
|
+
digest: "sha512"
|
|
7
|
+
};
|
|
8
|
+
function hashPassword(password, salt, config) {
|
|
9
|
+
const iterations = config?.iterations ?? DEFAULTS.iterations;
|
|
10
|
+
const keyLength = config?.keyLength ?? DEFAULTS.keyLength;
|
|
11
|
+
const digest = config?.digest ?? DEFAULTS.digest;
|
|
12
|
+
return crypto.pbkdf2Sync(password, salt, iterations, keyLength, digest).toString("hex");
|
|
13
|
+
}
|
|
14
|
+
function verifyPassword(password, salt, storedHash, config) {
|
|
15
|
+
const inputHash = hashPassword(password, salt, config);
|
|
16
|
+
const storedBuffer = Buffer.from(storedHash, "hex");
|
|
17
|
+
const inputBuffer = Buffer.from(inputHash, "hex");
|
|
18
|
+
if (storedBuffer.length !== inputBuffer.length) {
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
return crypto.timingSafeEqual(storedBuffer, inputBuffer);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// src/auth/token.ts
|
|
25
|
+
import crypto2 from "crypto";
|
|
26
|
+
var DEFAULT_TOKEN_BYTES = 32;
|
|
27
|
+
function generateSessionToken(bytes) {
|
|
28
|
+
return crypto2.randomBytes(bytes ?? DEFAULT_TOKEN_BYTES).toString("hex");
|
|
29
|
+
}
|
|
30
|
+
export {
|
|
31
|
+
generateSessionToken,
|
|
32
|
+
hashPassword,
|
|
33
|
+
verifyPassword
|
|
34
|
+
};
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/** Server-side session data stored in Redis */
|
|
2
|
+
interface Session {
|
|
3
|
+
createdAt: number;
|
|
4
|
+
expiresAt: number;
|
|
5
|
+
ip?: string;
|
|
6
|
+
userAgent?: string;
|
|
7
|
+
}
|
|
8
|
+
/** Result of an authentication attempt */
|
|
9
|
+
interface AuthResult {
|
|
10
|
+
success: boolean;
|
|
11
|
+
message: string;
|
|
12
|
+
sessionToken?: string;
|
|
13
|
+
expiresAt?: number;
|
|
14
|
+
}
|
|
15
|
+
/** Result of admin request verification */
|
|
16
|
+
type VerifyResult = {
|
|
17
|
+
authorized: true;
|
|
18
|
+
token: string;
|
|
19
|
+
} | {
|
|
20
|
+
authorized: false;
|
|
21
|
+
status: number;
|
|
22
|
+
message: string;
|
|
23
|
+
};
|
|
24
|
+
/** Rate limit check result */
|
|
25
|
+
interface RateLimitResult {
|
|
26
|
+
allowed: boolean;
|
|
27
|
+
remaining: number;
|
|
28
|
+
retryAfterMs?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export type { AuthResult as A, RateLimitResult as R, Session as S, VerifyResult as V };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/** Supabase connection config */
|
|
2
|
+
interface SupabaseConfig {
|
|
3
|
+
url: string;
|
|
4
|
+
anonKey: string;
|
|
5
|
+
serviceRoleKey?: string;
|
|
6
|
+
}
|
|
7
|
+
/** Cloudflare R2 storage config */
|
|
8
|
+
interface R2Config {
|
|
9
|
+
accountId: string;
|
|
10
|
+
accessKeyId: string;
|
|
11
|
+
secretAccessKey: string;
|
|
12
|
+
bucketName: string;
|
|
13
|
+
publicUrl: string;
|
|
14
|
+
}
|
|
15
|
+
/** Redis session store config */
|
|
16
|
+
interface SessionStoreConfig {
|
|
17
|
+
redisUrl: string;
|
|
18
|
+
redisToken: string;
|
|
19
|
+
/** Key namespace prefix (e.g. "marketplace:", "portfolio:") */
|
|
20
|
+
keyPrefix: string;
|
|
21
|
+
/** Session TTL in milliseconds. Default: 24 hours */
|
|
22
|
+
sessionDuration?: number;
|
|
23
|
+
}
|
|
24
|
+
/** Rate limiter config */
|
|
25
|
+
interface RateLimiterConfig {
|
|
26
|
+
redisUrl: string;
|
|
27
|
+
redisToken: string;
|
|
28
|
+
keyPrefix: string;
|
|
29
|
+
/** Max attempts before blocking. Default: 10 */
|
|
30
|
+
maxAttempts?: number;
|
|
31
|
+
/** Time window in milliseconds. Default: 15 minutes */
|
|
32
|
+
windowMs?: number;
|
|
33
|
+
}
|
|
34
|
+
/** Auth/password config */
|
|
35
|
+
interface AuthConfig {
|
|
36
|
+
/** Cookie name for admin session. Default: "admin_session" */
|
|
37
|
+
cookieName?: string;
|
|
38
|
+
pbkdf2?: {
|
|
39
|
+
iterations?: number;
|
|
40
|
+
keyLength?: number;
|
|
41
|
+
digest?: string;
|
|
42
|
+
};
|
|
43
|
+
/** Session token length in bytes. Default: 32 (64-char hex) */
|
|
44
|
+
tokenBytes?: number;
|
|
45
|
+
}
|
|
46
|
+
/** Client-side upload config (API endpoint paths) */
|
|
47
|
+
interface UploadConfig {
|
|
48
|
+
/** Endpoint that returns presigned R2 URL (e.g. "/api/admin/upload") */
|
|
49
|
+
uploadEndpoint: string;
|
|
50
|
+
/** Endpoint to register media in DB (e.g. "/api/admin/media") */
|
|
51
|
+
mediaEndpoint: string;
|
|
52
|
+
}
|
|
53
|
+
/** Next.js request verifier config */
|
|
54
|
+
interface RequestVerifierConfig {
|
|
55
|
+
cookieName?: string;
|
|
56
|
+
getSession: (token: string) => Promise<{
|
|
57
|
+
createdAt: number;
|
|
58
|
+
expiresAt: number;
|
|
59
|
+
} | undefined>;
|
|
60
|
+
unauthorizedMessage?: string;
|
|
61
|
+
sessionExpiredMessage?: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export type { AuthConfig as A, R2Config as R, SessionStoreConfig as S, UploadConfig as U, RateLimiterConfig as a, RequestVerifierConfig as b, SupabaseConfig as c };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
declare const HTTP_STATUS: {
|
|
2
|
+
readonly OK: 200;
|
|
3
|
+
readonly CREATED: 201;
|
|
4
|
+
readonly NO_CONTENT: 204;
|
|
5
|
+
readonly BAD_REQUEST: 400;
|
|
6
|
+
readonly UNAUTHORIZED: 401;
|
|
7
|
+
readonly FORBIDDEN: 403;
|
|
8
|
+
readonly NOT_FOUND: 404;
|
|
9
|
+
readonly CONFLICT: 409;
|
|
10
|
+
readonly TOO_MANY_REQUESTS: 429;
|
|
11
|
+
readonly INTERNAL_ERROR: 500;
|
|
12
|
+
};
|
|
13
|
+
type HttpStatus = (typeof HTTP_STATUS)[keyof typeof HTTP_STATUS];
|
|
14
|
+
|
|
15
|
+
export { HTTP_STATUS, type HttpStatus };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// src/http/status.ts
|
|
2
|
+
var HTTP_STATUS = {
|
|
3
|
+
OK: 200,
|
|
4
|
+
CREATED: 201,
|
|
5
|
+
NO_CONTENT: 204,
|
|
6
|
+
BAD_REQUEST: 400,
|
|
7
|
+
UNAUTHORIZED: 401,
|
|
8
|
+
FORBIDDEN: 403,
|
|
9
|
+
NOT_FOUND: 404,
|
|
10
|
+
CONFLICT: 409,
|
|
11
|
+
TOO_MANY_REQUESTS: 429,
|
|
12
|
+
INTERNAL_ERROR: 500
|
|
13
|
+
};
|
|
14
|
+
export {
|
|
15
|
+
HTTP_STATUS
|
|
16
|
+
};
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export { createAdminClient, createBrowserClient, createPublicClient } from './supabase/index.js';
|
|
2
|
+
export { UploadOptions, UploadResult, createR2Client, getR2Bucket, getR2PublicUrl, uploadFile } from './r2/index.js';
|
|
3
|
+
export { generateSessionToken, hashPassword, verifyPassword } from './auth/index.js';
|
|
4
|
+
export { RateLimiter, SessionStore, createRateLimiter, createSessionStore, getDefaultSessionDuration } from './session/index.js';
|
|
5
|
+
export { HTTP_STATUS, HttpStatus } from './http/index.js';
|
|
6
|
+
export { A as AuthConfig, R as R2Config, a as RateLimiterConfig, b as RequestVerifierConfig, S as SessionStoreConfig, c as SupabaseConfig, U as UploadConfig } from './config-qNdTlg1g.js';
|
|
7
|
+
export { M as MediaInsert, a as MediaRecord, b as MediaUpdate } from './media-ExBfXePZ.js';
|
|
8
|
+
export { A as AuthResult, R as RateLimitResult, S as Session, V as VerifyResult } from './auth-C8Nq_GmD.js';
|
|
9
|
+
import '@supabase/supabase-js';
|
|
10
|
+
import '@aws-sdk/client-s3';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
// src/supabase/client.ts
|
|
2
|
+
import { createClient } from "@supabase/supabase-js";
|
|
3
|
+
function createBrowserClient(config) {
|
|
4
|
+
return createClient(config.url, config.anonKey);
|
|
5
|
+
}
|
|
6
|
+
|
|
7
|
+
// src/supabase/server.ts
|
|
8
|
+
import { createClient as createClient2 } from "@supabase/supabase-js";
|
|
9
|
+
function createAdminClient(config) {
|
|
10
|
+
if (!config.serviceRoleKey) {
|
|
11
|
+
throw new Error("@digiko-npm/cms: serviceRoleKey is required for createAdminClient");
|
|
12
|
+
}
|
|
13
|
+
return createClient2(config.url, config.serviceRoleKey);
|
|
14
|
+
}
|
|
15
|
+
function createPublicClient(config) {
|
|
16
|
+
return createClient2(config.url, config.anonKey);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// src/r2/client.ts
|
|
20
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
21
|
+
function createR2Client(config) {
|
|
22
|
+
return new S3Client({
|
|
23
|
+
region: "auto",
|
|
24
|
+
endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
|
|
25
|
+
credentials: {
|
|
26
|
+
accessKeyId: config.accessKeyId,
|
|
27
|
+
secretAccessKey: config.secretAccessKey
|
|
28
|
+
},
|
|
29
|
+
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
30
|
+
responseChecksumValidation: "WHEN_REQUIRED"
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
function getR2Bucket(config) {
|
|
34
|
+
return config.bucketName;
|
|
35
|
+
}
|
|
36
|
+
function getR2PublicUrl(config) {
|
|
37
|
+
return config.publicUrl;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// src/r2/upload.ts
|
|
41
|
+
async function uploadFile(config, { file, folder = "media", onProgress }) {
|
|
42
|
+
const presignRes = await fetch(config.uploadEndpoint, {
|
|
43
|
+
method: "POST",
|
|
44
|
+
headers: { "Content-Type": "application/json" },
|
|
45
|
+
body: JSON.stringify({
|
|
46
|
+
filename: file.name,
|
|
47
|
+
contentType: file.type,
|
|
48
|
+
folder
|
|
49
|
+
})
|
|
50
|
+
});
|
|
51
|
+
if (!presignRes.ok) {
|
|
52
|
+
throw new Error(`Failed to get upload URL: ${presignRes.status}`);
|
|
53
|
+
}
|
|
54
|
+
const { uploadUrl, publicUrl, key } = await presignRes.json();
|
|
55
|
+
await uploadToR2(uploadUrl, file, file.type, onProgress);
|
|
56
|
+
const confirmRes = await fetch(config.mediaEndpoint, {
|
|
57
|
+
method: "POST",
|
|
58
|
+
headers: { "Content-Type": "application/json" },
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
filename: key.split("/").pop(),
|
|
61
|
+
original_name: file.name,
|
|
62
|
+
mime_type: file.type,
|
|
63
|
+
size_bytes: file.size,
|
|
64
|
+
url: publicUrl
|
|
65
|
+
})
|
|
66
|
+
});
|
|
67
|
+
if (!confirmRes.ok) {
|
|
68
|
+
throw new Error(`Failed to register upload: ${confirmRes.status}`);
|
|
69
|
+
}
|
|
70
|
+
const media = await confirmRes.json();
|
|
71
|
+
return { url: publicUrl, key, media };
|
|
72
|
+
}
|
|
73
|
+
function uploadToR2(url, file, contentType, onProgress) {
|
|
74
|
+
return new Promise((resolve, reject) => {
|
|
75
|
+
const xhr = new XMLHttpRequest();
|
|
76
|
+
xhr.upload.addEventListener("progress", (e) => {
|
|
77
|
+
if (e.lengthComputable && onProgress) {
|
|
78
|
+
onProgress(Math.round(e.loaded / e.total * 100));
|
|
79
|
+
}
|
|
80
|
+
});
|
|
81
|
+
xhr.addEventListener("load", () => {
|
|
82
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
83
|
+
resolve();
|
|
84
|
+
} else {
|
|
85
|
+
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
xhr.addEventListener("error", () => reject(new Error("Upload failed")));
|
|
89
|
+
xhr.addEventListener("abort", () => reject(new Error("Upload aborted")));
|
|
90
|
+
xhr.open("PUT", url);
|
|
91
|
+
xhr.setRequestHeader("Content-Type", contentType);
|
|
92
|
+
xhr.send(file);
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// src/auth/password.ts
|
|
97
|
+
import crypto from "crypto";
|
|
98
|
+
var DEFAULTS = {
|
|
99
|
+
iterations: 1e5,
|
|
100
|
+
keyLength: 64,
|
|
101
|
+
digest: "sha512"
|
|
102
|
+
};
|
|
103
|
+
function hashPassword(password, salt, config) {
|
|
104
|
+
const iterations = config?.iterations ?? DEFAULTS.iterations;
|
|
105
|
+
const keyLength = config?.keyLength ?? DEFAULTS.keyLength;
|
|
106
|
+
const digest = config?.digest ?? DEFAULTS.digest;
|
|
107
|
+
return crypto.pbkdf2Sync(password, salt, iterations, keyLength, digest).toString("hex");
|
|
108
|
+
}
|
|
109
|
+
function verifyPassword(password, salt, storedHash, config) {
|
|
110
|
+
const inputHash = hashPassword(password, salt, config);
|
|
111
|
+
const storedBuffer = Buffer.from(storedHash, "hex");
|
|
112
|
+
const inputBuffer = Buffer.from(inputHash, "hex");
|
|
113
|
+
if (storedBuffer.length !== inputBuffer.length) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
return crypto.timingSafeEqual(storedBuffer, inputBuffer);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/auth/token.ts
|
|
120
|
+
import crypto2 from "crypto";
|
|
121
|
+
var DEFAULT_TOKEN_BYTES = 32;
|
|
122
|
+
function generateSessionToken(bytes) {
|
|
123
|
+
return crypto2.randomBytes(bytes ?? DEFAULT_TOKEN_BYTES).toString("hex");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/session/store.ts
|
|
127
|
+
import { Redis } from "@upstash/redis";
|
|
128
|
+
var DEFAULT_SESSION_DURATION = 24 * 60 * 60 * 1e3;
|
|
129
|
+
function createSessionStore(config) {
|
|
130
|
+
const redis = new Redis({
|
|
131
|
+
url: config.redisUrl,
|
|
132
|
+
token: config.redisToken
|
|
133
|
+
});
|
|
134
|
+
const sessionKey = (token) => `${config.keyPrefix}session:${token}`;
|
|
135
|
+
return {
|
|
136
|
+
async addSession(token, session) {
|
|
137
|
+
const ttlMs = session.expiresAt - Date.now();
|
|
138
|
+
const ttlSeconds = Math.max(Math.ceil(ttlMs / 1e3), 1);
|
|
139
|
+
await redis.set(sessionKey(token), JSON.stringify(session), { ex: ttlSeconds });
|
|
140
|
+
},
|
|
141
|
+
async getSession(token) {
|
|
142
|
+
const data = await redis.get(sessionKey(token));
|
|
143
|
+
if (!data) return void 0;
|
|
144
|
+
const session = typeof data === "string" ? JSON.parse(data) : data;
|
|
145
|
+
if (Date.now() > session.expiresAt) {
|
|
146
|
+
await redis.del(sessionKey(token));
|
|
147
|
+
return void 0;
|
|
148
|
+
}
|
|
149
|
+
return session;
|
|
150
|
+
},
|
|
151
|
+
async removeSession(token) {
|
|
152
|
+
const result = await redis.del(sessionKey(token));
|
|
153
|
+
return result > 0;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
function getDefaultSessionDuration() {
|
|
158
|
+
return DEFAULT_SESSION_DURATION;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
// src/session/rate-limit.ts
|
|
162
|
+
import { Redis as Redis2 } from "@upstash/redis";
|
|
163
|
+
var DEFAULTS2 = {
|
|
164
|
+
maxAttempts: 10,
|
|
165
|
+
windowMs: 15 * 60 * 1e3
|
|
166
|
+
// 15 minutes
|
|
167
|
+
};
|
|
168
|
+
function createRateLimiter(config) {
|
|
169
|
+
const redis = new Redis2({
|
|
170
|
+
url: config.redisUrl,
|
|
171
|
+
token: config.redisToken
|
|
172
|
+
});
|
|
173
|
+
const maxAttempts = config.maxAttempts ?? DEFAULTS2.maxAttempts;
|
|
174
|
+
const windowMs = config.windowMs ?? DEFAULTS2.windowMs;
|
|
175
|
+
const rateLimitKey = (key) => `${config.keyPrefix}ratelimit:${key}`;
|
|
176
|
+
return {
|
|
177
|
+
async check(key) {
|
|
178
|
+
const now = Date.now();
|
|
179
|
+
const redisKey = rateLimitKey(key);
|
|
180
|
+
const data = await redis.get(redisKey);
|
|
181
|
+
let entry = null;
|
|
182
|
+
if (data) {
|
|
183
|
+
entry = typeof data === "string" ? JSON.parse(data) : data;
|
|
184
|
+
if (entry && now > entry.resetAt) entry = null;
|
|
185
|
+
}
|
|
186
|
+
if (!entry) {
|
|
187
|
+
const newEntry = { count: 1, resetAt: now + windowMs };
|
|
188
|
+
await redis.set(redisKey, JSON.stringify(newEntry), { ex: Math.ceil(windowMs / 1e3) });
|
|
189
|
+
return { allowed: true, remaining: maxAttempts - 1 };
|
|
190
|
+
}
|
|
191
|
+
if (entry.count >= maxAttempts) {
|
|
192
|
+
return { allowed: false, remaining: 0, retryAfterMs: entry.resetAt - now };
|
|
193
|
+
}
|
|
194
|
+
entry.count++;
|
|
195
|
+
const ttlSeconds = Math.max(Math.ceil((entry.resetAt - now) / 1e3), 1);
|
|
196
|
+
await redis.set(redisKey, JSON.stringify(entry), { ex: ttlSeconds });
|
|
197
|
+
return { allowed: true, remaining: maxAttempts - entry.count };
|
|
198
|
+
},
|
|
199
|
+
async reset(key) {
|
|
200
|
+
await redis.del(rateLimitKey(key));
|
|
201
|
+
}
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// src/http/status.ts
|
|
206
|
+
var HTTP_STATUS = {
|
|
207
|
+
OK: 200,
|
|
208
|
+
CREATED: 201,
|
|
209
|
+
NO_CONTENT: 204,
|
|
210
|
+
BAD_REQUEST: 400,
|
|
211
|
+
UNAUTHORIZED: 401,
|
|
212
|
+
FORBIDDEN: 403,
|
|
213
|
+
NOT_FOUND: 404,
|
|
214
|
+
CONFLICT: 409,
|
|
215
|
+
TOO_MANY_REQUESTS: 429,
|
|
216
|
+
INTERNAL_ERROR: 500
|
|
217
|
+
};
|
|
218
|
+
export {
|
|
219
|
+
HTTP_STATUS,
|
|
220
|
+
createAdminClient,
|
|
221
|
+
createBrowserClient,
|
|
222
|
+
createPublicClient,
|
|
223
|
+
createR2Client,
|
|
224
|
+
createRateLimiter,
|
|
225
|
+
createSessionStore,
|
|
226
|
+
generateSessionToken,
|
|
227
|
+
getDefaultSessionDuration,
|
|
228
|
+
getR2Bucket,
|
|
229
|
+
getR2PublicUrl,
|
|
230
|
+
hashPassword,
|
|
231
|
+
uploadFile,
|
|
232
|
+
verifyPassword
|
|
233
|
+
};
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Generic media record — shared across all projects that use R2 uploads */
|
|
2
|
+
interface MediaRecord {
|
|
3
|
+
id: string;
|
|
4
|
+
filename: string;
|
|
5
|
+
original_name: string;
|
|
6
|
+
mime_type: string;
|
|
7
|
+
size_bytes: number;
|
|
8
|
+
url: string;
|
|
9
|
+
width?: number | null;
|
|
10
|
+
height?: number | null;
|
|
11
|
+
alt_text?: string | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
}
|
|
14
|
+
/** Fields required when creating a media record */
|
|
15
|
+
type MediaInsert = Omit<MediaRecord, 'id' | 'created_at'>;
|
|
16
|
+
/** Fields that can be updated on an existing media record */
|
|
17
|
+
type MediaUpdate = Partial<Pick<MediaRecord, 'alt_text' | 'filename'>>;
|
|
18
|
+
|
|
19
|
+
export type { MediaInsert as M, MediaRecord as a, MediaUpdate as b };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from 'next/server';
|
|
2
|
+
import { b as RequestVerifierConfig } from '../config-qNdTlg1g.js';
|
|
3
|
+
|
|
4
|
+
type AuthSuccess = {
|
|
5
|
+
authorized: true;
|
|
6
|
+
token: string;
|
|
7
|
+
};
|
|
8
|
+
type AuthFailure = {
|
|
9
|
+
authorized: false;
|
|
10
|
+
response: NextResponse;
|
|
11
|
+
};
|
|
12
|
+
/**
|
|
13
|
+
* Create a request verifier for Next.js API routes.
|
|
14
|
+
* Extracts session token from cookie or Authorization header,
|
|
15
|
+
* then validates against the provided session store.
|
|
16
|
+
*/
|
|
17
|
+
declare function createRequestVerifier(config: RequestVerifierConfig): (request: NextRequest) => Promise<AuthSuccess | AuthFailure>;
|
|
18
|
+
|
|
19
|
+
export { createRequestVerifier };
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// src/next/verify-request.ts
|
|
2
|
+
import { NextResponse } from "next/server";
|
|
3
|
+
|
|
4
|
+
// src/http/status.ts
|
|
5
|
+
var HTTP_STATUS = {
|
|
6
|
+
OK: 200,
|
|
7
|
+
CREATED: 201,
|
|
8
|
+
NO_CONTENT: 204,
|
|
9
|
+
BAD_REQUEST: 400,
|
|
10
|
+
UNAUTHORIZED: 401,
|
|
11
|
+
FORBIDDEN: 403,
|
|
12
|
+
NOT_FOUND: 404,
|
|
13
|
+
CONFLICT: 409,
|
|
14
|
+
TOO_MANY_REQUESTS: 429,
|
|
15
|
+
INTERNAL_ERROR: 500
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
// src/next/verify-request.ts
|
|
19
|
+
var DEFAULTS = {
|
|
20
|
+
cookieName: "admin_session",
|
|
21
|
+
unauthorizedMessage: "Unauthorized",
|
|
22
|
+
sessionExpiredMessage: "Session expired"
|
|
23
|
+
};
|
|
24
|
+
function createRequestVerifier(config) {
|
|
25
|
+
const cookieName = config.cookieName ?? DEFAULTS.cookieName;
|
|
26
|
+
const unauthorizedMsg = config.unauthorizedMessage ?? DEFAULTS.unauthorizedMessage;
|
|
27
|
+
const sessionExpiredMsg = config.sessionExpiredMessage ?? DEFAULTS.sessionExpiredMessage;
|
|
28
|
+
return async function verifyAdminRequest(request) {
|
|
29
|
+
const token = request.cookies.get(cookieName)?.value || request.headers.get("Authorization")?.replace("Bearer ", "");
|
|
30
|
+
if (!token) {
|
|
31
|
+
return {
|
|
32
|
+
authorized: false,
|
|
33
|
+
response: NextResponse.json(
|
|
34
|
+
{ error: unauthorizedMsg },
|
|
35
|
+
{ status: HTTP_STATUS.UNAUTHORIZED }
|
|
36
|
+
)
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
const session = await config.getSession(token);
|
|
40
|
+
if (!session) {
|
|
41
|
+
return {
|
|
42
|
+
authorized: false,
|
|
43
|
+
response: NextResponse.json(
|
|
44
|
+
{ error: sessionExpiredMsg },
|
|
45
|
+
{ status: HTTP_STATUS.UNAUTHORIZED }
|
|
46
|
+
)
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
return { authorized: true, token };
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
export {
|
|
53
|
+
createRequestVerifier
|
|
54
|
+
};
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { S3Client } from '@aws-sdk/client-s3';
|
|
2
|
+
import { R as R2Config, U as UploadConfig } from '../config-qNdTlg1g.js';
|
|
3
|
+
import { a as MediaRecord } from '../media-ExBfXePZ.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Create a Cloudflare R2 client (S3-compatible).
|
|
7
|
+
*/
|
|
8
|
+
declare function createR2Client(config: R2Config): S3Client;
|
|
9
|
+
/** Get the R2 bucket name from config */
|
|
10
|
+
declare function getR2Bucket(config: R2Config): string;
|
|
11
|
+
/** Get the R2 public URL from config */
|
|
12
|
+
declare function getR2PublicUrl(config: R2Config): string;
|
|
13
|
+
|
|
14
|
+
interface UploadOptions {
|
|
15
|
+
file: File;
|
|
16
|
+
folder?: string;
|
|
17
|
+
onProgress?: (percent: number) => void;
|
|
18
|
+
}
|
|
19
|
+
interface UploadResult {
|
|
20
|
+
url: string;
|
|
21
|
+
key: string;
|
|
22
|
+
media: MediaRecord;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Upload a file to R2 via presigned URL, then register in the DB.
|
|
26
|
+
*
|
|
27
|
+
* Flow:
|
|
28
|
+
* 1. POST to uploadEndpoint → get presigned PUT URL
|
|
29
|
+
* 2. PUT file directly to R2 (with progress tracking via XHR)
|
|
30
|
+
* 3. POST metadata to mediaEndpoint → register the upload
|
|
31
|
+
*/
|
|
32
|
+
declare function uploadFile(config: UploadConfig, { file, folder, onProgress }: UploadOptions): Promise<UploadResult>;
|
|
33
|
+
|
|
34
|
+
export { type UploadOptions, type UploadResult, createR2Client, getR2Bucket, getR2PublicUrl, uploadFile };
|
package/dist/r2/index.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
// src/r2/client.ts
|
|
2
|
+
import { S3Client } from "@aws-sdk/client-s3";
|
|
3
|
+
function createR2Client(config) {
|
|
4
|
+
return new S3Client({
|
|
5
|
+
region: "auto",
|
|
6
|
+
endpoint: `https://${config.accountId}.r2.cloudflarestorage.com`,
|
|
7
|
+
credentials: {
|
|
8
|
+
accessKeyId: config.accessKeyId,
|
|
9
|
+
secretAccessKey: config.secretAccessKey
|
|
10
|
+
},
|
|
11
|
+
requestChecksumCalculation: "WHEN_REQUIRED",
|
|
12
|
+
responseChecksumValidation: "WHEN_REQUIRED"
|
|
13
|
+
});
|
|
14
|
+
}
|
|
15
|
+
function getR2Bucket(config) {
|
|
16
|
+
return config.bucketName;
|
|
17
|
+
}
|
|
18
|
+
function getR2PublicUrl(config) {
|
|
19
|
+
return config.publicUrl;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
// src/r2/upload.ts
|
|
23
|
+
async function uploadFile(config, { file, folder = "media", onProgress }) {
|
|
24
|
+
const presignRes = await fetch(config.uploadEndpoint, {
|
|
25
|
+
method: "POST",
|
|
26
|
+
headers: { "Content-Type": "application/json" },
|
|
27
|
+
body: JSON.stringify({
|
|
28
|
+
filename: file.name,
|
|
29
|
+
contentType: file.type,
|
|
30
|
+
folder
|
|
31
|
+
})
|
|
32
|
+
});
|
|
33
|
+
if (!presignRes.ok) {
|
|
34
|
+
throw new Error(`Failed to get upload URL: ${presignRes.status}`);
|
|
35
|
+
}
|
|
36
|
+
const { uploadUrl, publicUrl, key } = await presignRes.json();
|
|
37
|
+
await uploadToR2(uploadUrl, file, file.type, onProgress);
|
|
38
|
+
const confirmRes = await fetch(config.mediaEndpoint, {
|
|
39
|
+
method: "POST",
|
|
40
|
+
headers: { "Content-Type": "application/json" },
|
|
41
|
+
body: JSON.stringify({
|
|
42
|
+
filename: key.split("/").pop(),
|
|
43
|
+
original_name: file.name,
|
|
44
|
+
mime_type: file.type,
|
|
45
|
+
size_bytes: file.size,
|
|
46
|
+
url: publicUrl
|
|
47
|
+
})
|
|
48
|
+
});
|
|
49
|
+
if (!confirmRes.ok) {
|
|
50
|
+
throw new Error(`Failed to register upload: ${confirmRes.status}`);
|
|
51
|
+
}
|
|
52
|
+
const media = await confirmRes.json();
|
|
53
|
+
return { url: publicUrl, key, media };
|
|
54
|
+
}
|
|
55
|
+
function uploadToR2(url, file, contentType, onProgress) {
|
|
56
|
+
return new Promise((resolve, reject) => {
|
|
57
|
+
const xhr = new XMLHttpRequest();
|
|
58
|
+
xhr.upload.addEventListener("progress", (e) => {
|
|
59
|
+
if (e.lengthComputable && onProgress) {
|
|
60
|
+
onProgress(Math.round(e.loaded / e.total * 100));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
xhr.addEventListener("load", () => {
|
|
64
|
+
if (xhr.status >= 200 && xhr.status < 300) {
|
|
65
|
+
resolve();
|
|
66
|
+
} else {
|
|
67
|
+
reject(new Error(`Upload failed with status ${xhr.status}`));
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
xhr.addEventListener("error", () => reject(new Error("Upload failed")));
|
|
71
|
+
xhr.addEventListener("abort", () => reject(new Error("Upload aborted")));
|
|
72
|
+
xhr.open("PUT", url);
|
|
73
|
+
xhr.setRequestHeader("Content-Type", contentType);
|
|
74
|
+
xhr.send(file);
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
createR2Client,
|
|
79
|
+
getR2Bucket,
|
|
80
|
+
getR2PublicUrl,
|
|
81
|
+
uploadFile
|
|
82
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { S as SessionStoreConfig, a as RateLimiterConfig } from '../config-qNdTlg1g.js';
|
|
2
|
+
import { S as Session, R as RateLimitResult } from '../auth-C8Nq_GmD.js';
|
|
3
|
+
|
|
4
|
+
interface SessionStore {
|
|
5
|
+
addSession: (token: string, session: Session) => Promise<void>;
|
|
6
|
+
getSession: (token: string) => Promise<Session | undefined>;
|
|
7
|
+
removeSession: (token: string) => Promise<boolean>;
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Create a Redis-backed session store.
|
|
11
|
+
* All keys are namespaced with the configured keyPrefix.
|
|
12
|
+
*/
|
|
13
|
+
declare function createSessionStore(config: SessionStoreConfig): SessionStore;
|
|
14
|
+
/** Get the default session duration in milliseconds */
|
|
15
|
+
declare function getDefaultSessionDuration(): number;
|
|
16
|
+
|
|
17
|
+
interface RateLimiter {
|
|
18
|
+
check: (key: string) => Promise<RateLimitResult>;
|
|
19
|
+
reset: (key: string) => Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* Create a Redis-backed sliding-window rate limiter.
|
|
23
|
+
* Keys are namespaced with the configured keyPrefix.
|
|
24
|
+
*/
|
|
25
|
+
declare function createRateLimiter(config: RateLimiterConfig): RateLimiter;
|
|
26
|
+
|
|
27
|
+
export { type RateLimiter, type SessionStore, createRateLimiter, createSessionStore, getDefaultSessionDuration };
|