@ultrst-armada/kloks 1.7.0 → 1.8.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/.krusbl-release-attestation.json +3 -3
- package/package.json +1 -1
- package/packages/backend/dist/{BaseStorageAdapter-tosOoSUg.d.ts → BaseStorageAdapter-EUCSvJse.d.ts} +7 -1
- package/packages/backend/dist/auth/index.d.ts +51 -13
- package/packages/backend/dist/auth/index.js +6 -2
- package/packages/backend/dist/{chunk-BI5RDDB4.js → chunk-3IUA66RS.js} +9 -2
- package/packages/backend/dist/{chunk-KSZ67VZJ.js → chunk-5RXXC6GE.js} +3 -3
- package/packages/backend/dist/{chunk-Y3X3H2XX.js → chunk-BPFZCDS4.js} +12 -12
- package/packages/backend/dist/{chunk-6QTNNZSE.js → chunk-BTMIK44G.js} +136 -71
- package/packages/backend/dist/{chunk-MPYASJ5A.js → chunk-HT2SEDHD.js} +1 -1
- package/packages/backend/dist/chunk-KKRWZDMN.js +237 -0
- package/packages/backend/dist/chunk-M4GSR46Y.js +205 -0
- package/packages/backend/dist/chunk-RBQ4CW3T.js +136 -0
- package/packages/backend/dist/{chunk-J2J24JVC.js → chunk-SHZAQTIT.js} +31 -204
- package/packages/backend/dist/{chunk-N4Z57FVR.js → chunk-XHADDA75.js} +9 -3
- package/packages/backend/dist/index.d.ts +5 -5
- package/packages/backend/dist/index.js +65 -50
- package/packages/backend/dist/krnl-services.js +7 -6
- package/packages/backend/dist/services/index.js +3 -2
- package/packages/backend/dist/srkl/index.d.ts +7 -1
- package/packages/backend/dist/srkl/index.js +3 -3
- package/packages/backend/dist/storage/index.d.ts +35 -3
- package/packages/backend/dist/storage/index.js +21 -10
- package/packages/backend/dist/workspace/index.d.ts +27 -33
- package/packages/backend/dist/workspace/index.js +2 -1
- package/packages/client/dist/index.d.ts +34 -1
- package/packages/client/dist/index.js +116 -0
- package/packages/backend/dist/chunk-LS3HWJBX.js +0 -0
- package/packages/backend/dist/chunk-ZDH4XI7W.js +0 -140
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
// src/storage/BaseStorageAdapter.ts
|
|
2
|
+
import { SkipoBase } from "@ultrst-armada/kloks/functions";
|
|
3
|
+
var BaseStorageAdapter = class extends SkipoBase {
|
|
4
|
+
config;
|
|
5
|
+
constructor(config) {
|
|
6
|
+
super({ name: "BaseStorageAdapter" });
|
|
7
|
+
this.config = config;
|
|
8
|
+
}
|
|
9
|
+
async deleteFolder(pathStr) {
|
|
10
|
+
const items = await this.list(pathStr);
|
|
11
|
+
let count = 0;
|
|
12
|
+
for (const item of items) {
|
|
13
|
+
await this.delete(item.path);
|
|
14
|
+
count++;
|
|
15
|
+
}
|
|
16
|
+
return { success: true, deletedCount: count };
|
|
17
|
+
}
|
|
18
|
+
async renameFolder(oldPath, newPath) {
|
|
19
|
+
const items = await this.list(oldPath);
|
|
20
|
+
let count = 0;
|
|
21
|
+
for (const item of items) {
|
|
22
|
+
const rel = item.path.startsWith(oldPath) ? item.path.slice(oldPath.length).replace(/^\/+/, "") : item.path;
|
|
23
|
+
const target = `${newPath.replace(/\/+$/, "")}/${rel}`;
|
|
24
|
+
const data = await this.read(item.path);
|
|
25
|
+
await this.write(target, data);
|
|
26
|
+
await this.delete(item.path);
|
|
27
|
+
count++;
|
|
28
|
+
}
|
|
29
|
+
return { success: true, movedCount: count };
|
|
30
|
+
}
|
|
31
|
+
async ensureDirectory(pathStr) {
|
|
32
|
+
await this.mkdir(pathStr);
|
|
33
|
+
}
|
|
34
|
+
async testConnection() {
|
|
35
|
+
const start = performance.now();
|
|
36
|
+
try {
|
|
37
|
+
await this.list("");
|
|
38
|
+
return { ok: true, latencyMs: Math.round(performance.now() - start) };
|
|
39
|
+
} catch (e) {
|
|
40
|
+
return { ok: false, latencyMs: Math.round(performance.now() - start), error: e.message };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
normalizePath(p) {
|
|
44
|
+
return p.replace(/\\/g, "/").replace(/^\/+/, "");
|
|
45
|
+
}
|
|
46
|
+
getExtension(p) {
|
|
47
|
+
const parts = p.split(".");
|
|
48
|
+
return parts.length > 1 ? parts[parts.length - 1] : "";
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/storage/GoogleDriveOAuth.ts
|
|
53
|
+
var AUTH_URL = "https://accounts.google.com/o/oauth2/v2/auth";
|
|
54
|
+
var TOKEN_URL = "https://oauth2.googleapis.com/token";
|
|
55
|
+
function createGoogleDriveAuthorizationUrl(config, state, codeChallenge) {
|
|
56
|
+
const params = new URLSearchParams({
|
|
57
|
+
client_id: config.clientId,
|
|
58
|
+
redirect_uri: config.redirectUri,
|
|
59
|
+
response_type: "code",
|
|
60
|
+
access_type: "offline",
|
|
61
|
+
prompt: "consent",
|
|
62
|
+
scope: (config.scopes || ["https://www.googleapis.com/auth/drive.file"]).join(" "),
|
|
63
|
+
state
|
|
64
|
+
});
|
|
65
|
+
if (codeChallenge) {
|
|
66
|
+
params.set("code_challenge", codeChallenge);
|
|
67
|
+
params.set("code_challenge_method", "S256");
|
|
68
|
+
}
|
|
69
|
+
return `${AUTH_URL}?${params.toString()}`;
|
|
70
|
+
}
|
|
71
|
+
async function exchangeGoogleDriveCode(config, code, codeVerifier) {
|
|
72
|
+
const body = new URLSearchParams({
|
|
73
|
+
client_id: config.clientId,
|
|
74
|
+
redirect_uri: config.redirectUri,
|
|
75
|
+
code,
|
|
76
|
+
grant_type: "authorization_code"
|
|
77
|
+
});
|
|
78
|
+
if (config.clientSecret) body.set("client_secret", config.clientSecret);
|
|
79
|
+
if (codeVerifier) body.set("code_verifier", codeVerifier);
|
|
80
|
+
const response = await fetch(TOKEN_URL, {
|
|
81
|
+
method: "POST",
|
|
82
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
83
|
+
body
|
|
84
|
+
});
|
|
85
|
+
if (!response.ok) throw new Error(`Google OAuth token exchange failed: ${response.status}`);
|
|
86
|
+
const data = await response.json();
|
|
87
|
+
return {
|
|
88
|
+
accessToken: data.access_token,
|
|
89
|
+
refreshToken: data.refresh_token,
|
|
90
|
+
expiresAt: data.expires_in ? Date.now() + Number(data.expires_in) * 1e3 : void 0,
|
|
91
|
+
tokenType: data.token_type
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
async function refreshGoogleDriveAccessToken(clientId, refreshToken) {
|
|
95
|
+
const body = new URLSearchParams({
|
|
96
|
+
client_id: clientId,
|
|
97
|
+
refresh_token: refreshToken,
|
|
98
|
+
grant_type: "refresh_token"
|
|
99
|
+
});
|
|
100
|
+
const response = await fetch(TOKEN_URL, {
|
|
101
|
+
method: "POST",
|
|
102
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
103
|
+
body
|
|
104
|
+
});
|
|
105
|
+
if (!response.ok) throw new Error(`Google OAuth refresh failed: ${response.status}`);
|
|
106
|
+
const data = await response.json();
|
|
107
|
+
return {
|
|
108
|
+
accessToken: data.access_token,
|
|
109
|
+
expiresAt: data.expires_in ? Date.now() + Number(data.expires_in) * 1e3 : void 0,
|
|
110
|
+
tokenType: data.token_type
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// src/storage/GoogleDriveAdapter.ts
|
|
115
|
+
var GoogleDriveAdapter = class extends BaseStorageAdapter {
|
|
116
|
+
async token() {
|
|
117
|
+
const accessToken = this.config.googleAccessToken || this.config.accessKey;
|
|
118
|
+
const expiresAt = Number(this.config.googleAccessTokenExpiresAt || 0);
|
|
119
|
+
if (accessToken && (!expiresAt || expiresAt > Date.now() + 6e4)) return accessToken;
|
|
120
|
+
const refreshToken = this.config.googleRefreshToken;
|
|
121
|
+
const clientId = this.config.googleClientId;
|
|
122
|
+
if (!refreshToken || !clientId) throw new Error("Google Drive OAuth credentials are incomplete.");
|
|
123
|
+
const refreshed = await refreshGoogleDriveAccessToken(clientId, refreshToken);
|
|
124
|
+
this.config.googleAccessToken = refreshed.accessToken;
|
|
125
|
+
this.config.googleAccessTokenExpiresAt = refreshed.expiresAt;
|
|
126
|
+
return refreshed.accessToken;
|
|
127
|
+
}
|
|
128
|
+
async request(url, init = {}) {
|
|
129
|
+
const headers = new Headers(init.headers);
|
|
130
|
+
headers.set("Authorization", `Bearer ${await this.token()}`);
|
|
131
|
+
return fetch(url, { ...init, headers });
|
|
132
|
+
}
|
|
133
|
+
async findChild(parentId, name) {
|
|
134
|
+
const q = `'${parentId}' in parents and name = '${name.replace(/'/g, "\\'")}' and trashed = false`;
|
|
135
|
+
const res = await this.request(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=files(id,name,size,modifiedTime,mimeType)&pageSize=10`);
|
|
136
|
+
if (!res.ok) throw new Error(`Google Drive lookup failed: ${res.status}`);
|
|
137
|
+
const data = await res.json();
|
|
138
|
+
return data.files?.[0] || null;
|
|
139
|
+
}
|
|
140
|
+
async resolveParent(relativeDir, create = false) {
|
|
141
|
+
let parent = this.config.googleRootFolderId || this.config.rootFolderId || "root";
|
|
142
|
+
for (const part of this.normalizePath(relativeDir).split("/").filter(Boolean)) {
|
|
143
|
+
let child = await this.findChild(parent, part);
|
|
144
|
+
if (!child && create) {
|
|
145
|
+
const res = await this.request("https://www.googleapis.com/drive/v3/files", {
|
|
146
|
+
method: "POST",
|
|
147
|
+
headers: { "Content-Type": "application/json" },
|
|
148
|
+
body: JSON.stringify({ name: part, mimeType: "application/vnd.google-apps.folder", parents: [parent] })
|
|
149
|
+
});
|
|
150
|
+
if (!res.ok) throw new Error(`Google Drive folder creation failed: ${res.status}`);
|
|
151
|
+
child = await res.json();
|
|
152
|
+
}
|
|
153
|
+
if (!child) throw new Error(`Google Drive directory not found: ${part}`);
|
|
154
|
+
parent = child.id;
|
|
155
|
+
}
|
|
156
|
+
return parent;
|
|
157
|
+
}
|
|
158
|
+
split(pathStr) {
|
|
159
|
+
const key = this.normalizePath(pathStr);
|
|
160
|
+
const i = key.lastIndexOf("/");
|
|
161
|
+
return i < 0 ? { dir: "", name: key } : { dir: key.slice(0, i), name: key.slice(i + 1) };
|
|
162
|
+
}
|
|
163
|
+
async read(pathStr) {
|
|
164
|
+
const { dir, name } = this.split(pathStr);
|
|
165
|
+
const parent = await this.resolveParent(dir);
|
|
166
|
+
const file = await this.findChild(parent, name);
|
|
167
|
+
if (!file) throw new Error(`Google Drive file not found: ${pathStr}`);
|
|
168
|
+
const res = await this.request(`https://www.googleapis.com/drive/v3/files/${file.id}?alt=media`);
|
|
169
|
+
if (!res.ok) throw new Error(`Google Drive download failed: ${res.status}`);
|
|
170
|
+
return new Uint8Array(await res.arrayBuffer());
|
|
171
|
+
}
|
|
172
|
+
async write(pathStr, data, contentType = "application/octet-stream") {
|
|
173
|
+
const { dir, name } = this.split(pathStr);
|
|
174
|
+
const parent = await this.resolveParent(dir, true);
|
|
175
|
+
const existing = await this.findChild(parent, name);
|
|
176
|
+
const bytes = typeof data === "string" ? new TextEncoder().encode(data) : data;
|
|
177
|
+
const form = new FormData();
|
|
178
|
+
form.append("metadata", new Blob([JSON.stringify({ name, mimeType: contentType, ...existing ? {} : { parents: [parent] } })], { type: "application/json" }));
|
|
179
|
+
form.append("file", new Blob([bytes], { type: contentType }));
|
|
180
|
+
const url = existing ? `https://www.googleapis.com/upload/drive/v3/files/${existing.id}?uploadType=multipart` : "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart";
|
|
181
|
+
const res = await this.request(url, { method: existing ? "PATCH" : "POST", body: form });
|
|
182
|
+
if (!res.ok) throw new Error(`Google Drive upload failed: ${res.status}`);
|
|
183
|
+
}
|
|
184
|
+
async delete(pathStr) {
|
|
185
|
+
const { dir, name } = this.split(pathStr);
|
|
186
|
+
const parent = await this.resolveParent(dir);
|
|
187
|
+
const file = await this.findChild(parent, name);
|
|
188
|
+
if (!file) return;
|
|
189
|
+
const res = await this.request(`https://www.googleapis.com/drive/v3/files/${file.id}`, { method: "DELETE" });
|
|
190
|
+
if (!res.ok && res.status !== 404) throw new Error(`Google Drive delete failed: ${res.status}`);
|
|
191
|
+
}
|
|
192
|
+
async exists(pathStr) {
|
|
193
|
+
try {
|
|
194
|
+
await this.stat(pathStr);
|
|
195
|
+
return true;
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
async list(pathStr = "") {
|
|
201
|
+
const parent = await this.resolveParent(pathStr);
|
|
202
|
+
const q = `'${parent}' in parents and trashed = false`;
|
|
203
|
+
const res = await this.request(`https://www.googleapis.com/drive/v3/files?q=${encodeURIComponent(q)}&fields=files(id,name,size,modifiedTime,mimeType)&pageSize=1000`);
|
|
204
|
+
if (!res.ok) throw new Error(`Google Drive list failed: ${res.status}`);
|
|
205
|
+
const data = await res.json();
|
|
206
|
+
return (data.files || []).map((f) => ({ path: this.normalizePath(pathStr ? `${pathStr}/${f.name}` : f.name), size: Number(f.size || 0), modifiedAt: f.modifiedTime ? Date.parse(f.modifiedTime) : 0 }));
|
|
207
|
+
}
|
|
208
|
+
async stat(pathStr) {
|
|
209
|
+
const { dir, name } = this.split(pathStr);
|
|
210
|
+
const parent = await this.resolveParent(dir);
|
|
211
|
+
const file = await this.findChild(parent, name);
|
|
212
|
+
if (!file) throw new Error(`Google Drive file not found: ${pathStr}`);
|
|
213
|
+
return { path: pathStr, size: Number(file.size || 0), modifiedAt: file.modifiedTime ? Date.parse(file.modifiedTime) : 0 };
|
|
214
|
+
}
|
|
215
|
+
async mkdir(pathStr) {
|
|
216
|
+
await this.resolveParent(pathStr, true);
|
|
217
|
+
}
|
|
218
|
+
async testConnection() {
|
|
219
|
+
const start = performance.now();
|
|
220
|
+
try {
|
|
221
|
+
const res = await this.request("https://www.googleapis.com/drive/v3/about?fields=user");
|
|
222
|
+
if (!res.ok) throw new Error(`Google Drive authorization failed: ${res.status}`);
|
|
223
|
+
const data = await res.json();
|
|
224
|
+
return { ok: true, latencyMs: Math.round(performance.now() - start), details: { email: data.user?.emailAddress } };
|
|
225
|
+
} catch (e) {
|
|
226
|
+
return { ok: false, latencyMs: Math.round(performance.now() - start), error: e.message };
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
export {
|
|
232
|
+
BaseStorageAdapter,
|
|
233
|
+
createGoogleDriveAuthorizationUrl,
|
|
234
|
+
exchangeGoogleDriveCode,
|
|
235
|
+
refreshGoogleDriveAccessToken,
|
|
236
|
+
GoogleDriveAdapter
|
|
237
|
+
};
|
|
@@ -0,0 +1,205 @@
|
|
|
1
|
+
// src/auth/SessionManager.ts
|
|
2
|
+
var SessionManager = class {
|
|
3
|
+
ttl;
|
|
4
|
+
constructor(ttlMs = 864e5) {
|
|
5
|
+
this.ttl = ttlMs;
|
|
6
|
+
}
|
|
7
|
+
generateToken(userId) {
|
|
8
|
+
return `sess::${userId}::${Date.now()}::${Math.random().toString(36).substring(2, 9)}`;
|
|
9
|
+
}
|
|
10
|
+
createSession(userId, role) {
|
|
11
|
+
return {
|
|
12
|
+
token: this.generateToken(userId),
|
|
13
|
+
userId,
|
|
14
|
+
role,
|
|
15
|
+
expiresAt: Date.now() + this.ttl
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
isValid(session) {
|
|
19
|
+
return Date.now() < session.expiresAt;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/auth/RateLimiter.ts
|
|
24
|
+
var RateLimiter = class {
|
|
25
|
+
attempts = /* @__PURE__ */ new Map();
|
|
26
|
+
windowMs;
|
|
27
|
+
maxUser;
|
|
28
|
+
maxAdmin;
|
|
29
|
+
constructor(windowMs = 9e5, maxUser = 5, maxAdmin = 3) {
|
|
30
|
+
this.windowMs = windowMs;
|
|
31
|
+
this.maxUser = maxUser;
|
|
32
|
+
this.maxAdmin = maxAdmin;
|
|
33
|
+
}
|
|
34
|
+
check(identifier, isAdminAttempt = false) {
|
|
35
|
+
const now = Date.now();
|
|
36
|
+
let record = this.attempts.get(identifier);
|
|
37
|
+
if (!record || now - record.windowStart > this.windowMs) {
|
|
38
|
+
record = { count: 0, windowStart: now };
|
|
39
|
+
this.attempts.set(identifier, record);
|
|
40
|
+
}
|
|
41
|
+
record.count++;
|
|
42
|
+
const limit = isAdminAttempt ? this.maxAdmin : this.maxUser;
|
|
43
|
+
if (record.count > limit) {
|
|
44
|
+
throw new Error("Too many requests. Please try again later.");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
reset(identifier) {
|
|
48
|
+
this.attempts.delete(identifier);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
// src/auth/CanonicalAuthClient.ts
|
|
53
|
+
var cleanBaseUrl = (value) => value.trim().replace(/\/+$/, "");
|
|
54
|
+
var CanonicalAuthClient = class {
|
|
55
|
+
baseUrl;
|
|
56
|
+
fetcher;
|
|
57
|
+
credentials;
|
|
58
|
+
constructor(options) {
|
|
59
|
+
if (!options.baseUrl?.trim()) throw new Error("Canonical auth base URL is required.");
|
|
60
|
+
this.baseUrl = cleanBaseUrl(options.baseUrl);
|
|
61
|
+
this.fetcher = options.fetcher ?? fetch;
|
|
62
|
+
this.credentials = options.credentials ?? "include";
|
|
63
|
+
}
|
|
64
|
+
async request(path, init = {}) {
|
|
65
|
+
const response = await this.fetcher(`${this.baseUrl}/api/auth${path}`, {
|
|
66
|
+
...init,
|
|
67
|
+
credentials: this.credentials,
|
|
68
|
+
headers: { "Content-Type": "application/json", ...init.headers ?? {} }
|
|
69
|
+
});
|
|
70
|
+
const text = await response.text();
|
|
71
|
+
let data = {};
|
|
72
|
+
try {
|
|
73
|
+
data = text ? JSON.parse(text) : {};
|
|
74
|
+
} catch {
|
|
75
|
+
data = { message: text };
|
|
76
|
+
}
|
|
77
|
+
return { status: response.status, data };
|
|
78
|
+
}
|
|
79
|
+
async signInEmail(email, password) {
|
|
80
|
+
const result = await this.request("/sign-in/email", {
|
|
81
|
+
method: "POST",
|
|
82
|
+
body: JSON.stringify({ email: email.trim().toLowerCase(), password })
|
|
83
|
+
});
|
|
84
|
+
if (result.status >= 400 || result.data?.error) {
|
|
85
|
+
return { success: false, status: result.status, error: result.data?.error?.message ?? result.data?.message ?? "Authentication failed." };
|
|
86
|
+
}
|
|
87
|
+
return { success: true, status: result.status, user: result.data?.user, session: result.data?.session };
|
|
88
|
+
}
|
|
89
|
+
async signUpEmail(email, password, name) {
|
|
90
|
+
const result = await this.request("/sign-up/email", {
|
|
91
|
+
method: "POST",
|
|
92
|
+
body: JSON.stringify({ email: email.trim().toLowerCase(), password, name: name.trim() })
|
|
93
|
+
});
|
|
94
|
+
if (result.status >= 400 || result.data?.error) {
|
|
95
|
+
return { success: false, status: result.status, error: result.data?.error?.message ?? result.data?.message ?? "Registration failed." };
|
|
96
|
+
}
|
|
97
|
+
return { success: true, status: result.status, user: result.data?.user, session: result.data?.session };
|
|
98
|
+
}
|
|
99
|
+
async signInGoogle(callbackURL) {
|
|
100
|
+
const result = await this.request("/sign-in/social", {
|
|
101
|
+
method: "POST",
|
|
102
|
+
body: JSON.stringify({ provider: "google", callbackURL })
|
|
103
|
+
});
|
|
104
|
+
if (result.status >= 400 || result.data?.error) {
|
|
105
|
+
return { success: false, status: result.status, error: result.data?.error?.message ?? result.data?.message ?? "Google sign-in failed." };
|
|
106
|
+
}
|
|
107
|
+
return {
|
|
108
|
+
success: true,
|
|
109
|
+
status: result.status,
|
|
110
|
+
user: result.data?.user,
|
|
111
|
+
session: result.data?.session,
|
|
112
|
+
redirectUrl: result.data?.url ?? result.data?.redirect
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
async requestGoogleDriveAccess(callbackURL) {
|
|
116
|
+
const result = await this.request("/link-social", {
|
|
117
|
+
method: "POST",
|
|
118
|
+
body: JSON.stringify({
|
|
119
|
+
provider: "google",
|
|
120
|
+
scopes: ["https://www.googleapis.com/auth/drive.file"],
|
|
121
|
+
...callbackURL ? { callbackURL } : {}
|
|
122
|
+
})
|
|
123
|
+
});
|
|
124
|
+
if (result.status >= 400 || result.data?.error) {
|
|
125
|
+
return { success: false, status: result.status, error: result.data?.error?.message ?? result.data?.message ?? "Google Drive authorization failed." };
|
|
126
|
+
}
|
|
127
|
+
return { success: true, status: result.status, user: result.data?.user, session: result.data?.session };
|
|
128
|
+
}
|
|
129
|
+
async getSession(cookie) {
|
|
130
|
+
const headers = {};
|
|
131
|
+
if (cookie) headers.Cookie = cookie;
|
|
132
|
+
const result = await this.request("/get-session", { headers });
|
|
133
|
+
if (result.status >= 400 || !result.data?.user) return null;
|
|
134
|
+
return result.data;
|
|
135
|
+
}
|
|
136
|
+
async signOut() {
|
|
137
|
+
const result = await this.request("/sign-out", { method: "POST", body: "{}" });
|
|
138
|
+
return result.status < 400 && !result.data?.error;
|
|
139
|
+
}
|
|
140
|
+
};
|
|
141
|
+
function createCanonicalAuthClient(baseUrl, fetcher) {
|
|
142
|
+
return new CanonicalAuthClient({ baseUrl, fetcher });
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
// src/auth/UnifiedAuthCore.ts
|
|
146
|
+
function requireAuthBaseUrl(env) {
|
|
147
|
+
const baseUrl = String(env?.SRKL_AUTH_BASE_URL ?? (typeof process !== "undefined" ? process.env.SRKL_AUTH_BASE_URL : "") ?? "").trim();
|
|
148
|
+
if (!baseUrl) {
|
|
149
|
+
throw new Error("SRKL_AUTH_BASE_URL is required. Local application auth is not supported.");
|
|
150
|
+
}
|
|
151
|
+
return baseUrl;
|
|
152
|
+
}
|
|
153
|
+
async function loginSystemUser(email, rawPass, isAdmin, _queryDb, env) {
|
|
154
|
+
if (!email?.trim() || !rawPass) return { success: false, status: 400, error: "Email and password are required" };
|
|
155
|
+
try {
|
|
156
|
+
const client = createCanonicalAuthClient(requireAuthBaseUrl(env));
|
|
157
|
+
const result = await client.signInEmail(email, rawPass);
|
|
158
|
+
if (!result.success || !result.user) {
|
|
159
|
+
return { success: false, status: result.status, error: result.error ?? "Invalid email or password" };
|
|
160
|
+
}
|
|
161
|
+
if (isAdmin && result.user.role !== "admin") {
|
|
162
|
+
return { success: false, status: 403, error: "Insufficient administrative privileges" };
|
|
163
|
+
}
|
|
164
|
+
return {
|
|
165
|
+
success: true,
|
|
166
|
+
status: result.status,
|
|
167
|
+
user: {
|
|
168
|
+
...result.user,
|
|
169
|
+
username: result.user.username ?? result.user.email.split("@")[0],
|
|
170
|
+
role: result.user.role ?? "user",
|
|
171
|
+
status: result.user.status ?? "ACTIVE"
|
|
172
|
+
},
|
|
173
|
+
token: typeof result.session?.token === "string" ? result.session.token : void 0
|
|
174
|
+
};
|
|
175
|
+
} catch (error) {
|
|
176
|
+
return { success: false, status: 503, error: error instanceof Error ? error.message : "Canonical authentication service unavailable" };
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
async function resolveUserFromToken(tokenOrUserId, _queryDb, env) {
|
|
180
|
+
if (!tokenOrUserId) return null;
|
|
181
|
+
try {
|
|
182
|
+
const client = createCanonicalAuthClient(requireAuthBaseUrl(env));
|
|
183
|
+
const session = await client.getSession(
|
|
184
|
+
tokenOrUserId.startsWith("Bearer ") ? `better-auth.session_token=${tokenOrUserId.slice(7).trim()}` : void 0
|
|
185
|
+
);
|
|
186
|
+
if (!session?.user) return null;
|
|
187
|
+
return {
|
|
188
|
+
...session.user,
|
|
189
|
+
username: session.user.username ?? session.user.email.split("@")[0],
|
|
190
|
+
role: session.user.role ?? "user",
|
|
191
|
+
status: session.user.status ?? "ACTIVE"
|
|
192
|
+
};
|
|
193
|
+
} catch {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
export {
|
|
199
|
+
SessionManager,
|
|
200
|
+
RateLimiter,
|
|
201
|
+
CanonicalAuthClient,
|
|
202
|
+
createCanonicalAuthClient,
|
|
203
|
+
loginSystemUser,
|
|
204
|
+
resolveUserFromToken
|
|
205
|
+
};
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import {
|
|
2
|
+
ensureS3DirectoryMarker,
|
|
3
|
+
permanentDeleteS3Folder,
|
|
4
|
+
permanentDeleteS3Object,
|
|
5
|
+
renameS3Folder
|
|
6
|
+
} from "./chunk-SHZAQTIT.js";
|
|
7
|
+
import {
|
|
8
|
+
BaseStorageAdapter
|
|
9
|
+
} from "./chunk-KKRWZDMN.js";
|
|
10
|
+
|
|
11
|
+
// src/storage/S3Adapter.ts
|
|
12
|
+
var S3Client;
|
|
13
|
+
var PutObjectCommand;
|
|
14
|
+
var GetObjectCommand;
|
|
15
|
+
var ListObjectsV2Command;
|
|
16
|
+
var HeadObjectCommand;
|
|
17
|
+
var S3Adapter = class extends BaseStorageAdapter {
|
|
18
|
+
client;
|
|
19
|
+
bucket;
|
|
20
|
+
constructor(config) {
|
|
21
|
+
super(config);
|
|
22
|
+
this.bucket = config.bucket || "";
|
|
23
|
+
}
|
|
24
|
+
async ensureClient() {
|
|
25
|
+
if (this.client) return;
|
|
26
|
+
try {
|
|
27
|
+
const aws = await import("@aws-sdk/client-s3");
|
|
28
|
+
S3Client = aws.S3Client;
|
|
29
|
+
PutObjectCommand = aws.PutObjectCommand;
|
|
30
|
+
GetObjectCommand = aws.GetObjectCommand;
|
|
31
|
+
ListObjectsV2Command = aws.ListObjectsV2Command;
|
|
32
|
+
HeadObjectCommand = aws.HeadObjectCommand;
|
|
33
|
+
this.client = new S3Client({
|
|
34
|
+
region: this.config.region || "us-east-1",
|
|
35
|
+
endpoint: this.config.endpoint,
|
|
36
|
+
credentials: {
|
|
37
|
+
accessKeyId: this.config.accessKey,
|
|
38
|
+
secretAccessKey: this.config.secretKey
|
|
39
|
+
},
|
|
40
|
+
forcePathStyle: Boolean(this.config.endpoint)
|
|
41
|
+
});
|
|
42
|
+
} catch (e) {
|
|
43
|
+
throw new Error(`@aws-sdk/client-s3 failed to initialize: ${e.message}`);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
async read(pathStr) {
|
|
47
|
+
await this.ensureClient();
|
|
48
|
+
const command = new GetObjectCommand({ Bucket: this.bucket, Key: this.normalizePath(pathStr) });
|
|
49
|
+
const response = await this.client.send(command);
|
|
50
|
+
return response.Body ? await response.Body.transformToByteArray() : new Uint8Array();
|
|
51
|
+
}
|
|
52
|
+
async write(pathStr, data, contentType = "application/octet-stream") {
|
|
53
|
+
await this.ensureClient();
|
|
54
|
+
const body = typeof data === "string" ? Buffer.from(data, "utf-8") : Buffer.from(data);
|
|
55
|
+
const command = new PutObjectCommand({
|
|
56
|
+
Bucket: this.bucket,
|
|
57
|
+
Key: this.normalizePath(pathStr),
|
|
58
|
+
Body: body,
|
|
59
|
+
ContentType: contentType
|
|
60
|
+
});
|
|
61
|
+
await this.client.send(command);
|
|
62
|
+
}
|
|
63
|
+
async delete(pathStr) {
|
|
64
|
+
await this.ensureClient();
|
|
65
|
+
await permanentDeleteS3Object(this.client, this.bucket, this.normalizePath(pathStr));
|
|
66
|
+
}
|
|
67
|
+
async deleteFolder(pathStr) {
|
|
68
|
+
await this.ensureClient();
|
|
69
|
+
return await permanentDeleteS3Folder(this.client, this.bucket, this.normalizePath(pathStr));
|
|
70
|
+
}
|
|
71
|
+
async renameFolder(oldPath, newPath) {
|
|
72
|
+
await this.ensureClient();
|
|
73
|
+
return await renameS3Folder(this.client, this.bucket, this.normalizePath(oldPath), this.normalizePath(newPath));
|
|
74
|
+
}
|
|
75
|
+
async ensureDirectory(pathStr) {
|
|
76
|
+
await this.ensureClient();
|
|
77
|
+
await ensureS3DirectoryMarker(this.client, this.bucket, this.normalizePath(pathStr));
|
|
78
|
+
}
|
|
79
|
+
async exists(pathStr) {
|
|
80
|
+
await this.ensureClient();
|
|
81
|
+
try {
|
|
82
|
+
const command = new HeadObjectCommand({ Bucket: this.bucket, Key: this.normalizePath(pathStr) });
|
|
83
|
+
await this.client.send(command);
|
|
84
|
+
return true;
|
|
85
|
+
} catch (e) {
|
|
86
|
+
if (e.name === "NotFound" || e.$metadata?.httpStatusCode === 404) return false;
|
|
87
|
+
throw e;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async list(pathStr = "") {
|
|
91
|
+
await this.ensureClient();
|
|
92
|
+
const prefix = pathStr ? this.normalizePath(pathStr).replace(/\/+$/, "") + "/" : "";
|
|
93
|
+
const command = new ListObjectsV2Command({ Bucket: this.bucket, Prefix: prefix });
|
|
94
|
+
const response = await this.client.send(command);
|
|
95
|
+
return (response.Contents || []).map((item) => ({
|
|
96
|
+
path: item.Key,
|
|
97
|
+
size: item.Size || 0,
|
|
98
|
+
modifiedAt: item.LastModified ? item.LastModified.getTime() : 0
|
|
99
|
+
}));
|
|
100
|
+
}
|
|
101
|
+
async stat(pathStr) {
|
|
102
|
+
await this.ensureClient();
|
|
103
|
+
const command = new HeadObjectCommand({ Bucket: this.bucket, Key: this.normalizePath(pathStr) });
|
|
104
|
+
const response = await this.client.send(command);
|
|
105
|
+
return {
|
|
106
|
+
path: pathStr,
|
|
107
|
+
size: response.ContentLength || 0,
|
|
108
|
+
modifiedAt: response.LastModified ? response.LastModified.getTime() : 0
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
async mkdir(pathStr) {
|
|
112
|
+
await this.ensureDirectory(pathStr);
|
|
113
|
+
}
|
|
114
|
+
async testConnection() {
|
|
115
|
+
const start = performance.now();
|
|
116
|
+
try {
|
|
117
|
+
await this.ensureClient();
|
|
118
|
+
const res = await this.client.send(new ListObjectsV2Command({ Bucket: this.bucket, MaxKeys: 1 }));
|
|
119
|
+
return {
|
|
120
|
+
ok: true,
|
|
121
|
+
latencyMs: Math.round(performance.now() - start),
|
|
122
|
+
details: { bucket: this.bucket, keyCount: res.KeyCount ?? 0 }
|
|
123
|
+
};
|
|
124
|
+
} catch (e) {
|
|
125
|
+
return {
|
|
126
|
+
ok: false,
|
|
127
|
+
latencyMs: Math.round(performance.now() - start),
|
|
128
|
+
error: e.message
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
export {
|
|
135
|
+
S3Adapter
|
|
136
|
+
};
|