@ultrst-armada/kloks 1.7.1 → 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-6x_BQiB3.d.ts → BaseStorageAdapter-EUCSvJse.d.ts} +1 -0
- package/packages/backend/dist/auth/index.d.ts +51 -13
- package/packages/backend/dist/auth/index.js +6 -2
- package/packages/backend/dist/{chunk-F6IDQLC2.js → chunk-5RXXC6GE.js} +3 -3
- package/packages/backend/dist/{chunk-7ZT2ZPPH.js → chunk-BPFZCDS4.js} +11 -11
- package/packages/backend/dist/{chunk-6W7OAGNV.js → chunk-BTMIK44G.js} +84 -1
- 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-JUPWFW7V.js → chunk-RBQ4CW3T.js} +4 -2
- package/packages/backend/dist/{chunk-R45CEV46.js → chunk-SHZAQTIT.js} +4 -234
- package/packages/backend/dist/index.d.ts +3 -3
- package/packages/backend/dist/index.js +53 -47
- package/packages/backend/dist/krnl-services.js +6 -5
- package/packages/backend/dist/services/index.js +3 -2
- package/packages/backend/dist/srkl/index.js +1 -1
- package/packages/backend/dist/storage/index.d.ts +2 -2
- package/packages/backend/dist/storage/index.js +15 -13
- package/packages/backend/dist/workspace/index.d.ts +1 -1
- package/packages/backend/dist/workspace/index.js +2 -1
- package/packages/backend/dist/chunk-ZDH4XI7W.js +0 -140
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": 1,
|
|
3
3
|
"packageName": "@ultrst-armada/kloks",
|
|
4
|
-
"version": "1.
|
|
5
|
-
"sourceCommit": "
|
|
4
|
+
"version": "1.8.0",
|
|
5
|
+
"sourceCommit": "d364f4fd5a705bea93c4fb0a93ff9740c1050f49",
|
|
6
6
|
"releaseWorkflow": "npm Release",
|
|
7
|
-
"workflowRunId": "
|
|
7
|
+
"workflowRunId": "40208606705"
|
|
8
8
|
}
|
package/package.json
CHANGED
|
@@ -26,31 +26,69 @@ declare class AdminGuard extends SkipoBase {
|
|
|
26
26
|
static verifyUser(authHeaderOrToken: string | null | undefined, provider: BaseAccountProvider): Promise<AuthSession>;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
|
|
30
|
-
* @file UnifiedAuthCore.ts
|
|
31
|
-
* @description Strict Universal Authentication against "ultrst-users" and "ultrst-credentials".
|
|
32
|
-
* Zero offline dummy fallbacks. Runs across Node.js, Vercel, and Cloudflare Workers (< 200 lines).
|
|
33
|
-
*/
|
|
34
|
-
interface SystemUser {
|
|
29
|
+
interface CanonicalAuthUser {
|
|
35
30
|
id: string;
|
|
36
31
|
email: string;
|
|
37
|
-
username: string;
|
|
38
32
|
name: string;
|
|
33
|
+
username?: string;
|
|
34
|
+
role?: string;
|
|
35
|
+
status?: string;
|
|
36
|
+
appProfiles?: Record<string, unknown>;
|
|
37
|
+
emailVerified?: boolean;
|
|
38
|
+
}
|
|
39
|
+
interface CanonicalAuthSession {
|
|
40
|
+
session: Record<string, unknown>;
|
|
41
|
+
user: CanonicalAuthUser;
|
|
42
|
+
}
|
|
43
|
+
interface CanonicalAuthClientOptions {
|
|
44
|
+
baseUrl: string;
|
|
45
|
+
fetcher?: typeof fetch;
|
|
46
|
+
credentials?: RequestCredentials;
|
|
47
|
+
}
|
|
48
|
+
interface AuthResult {
|
|
49
|
+
success: boolean;
|
|
50
|
+
status: number;
|
|
51
|
+
user?: CanonicalAuthUser;
|
|
52
|
+
session?: Record<string, unknown>;
|
|
53
|
+
error?: string;
|
|
54
|
+
redirectUrl?: string;
|
|
55
|
+
}
|
|
56
|
+
declare class CanonicalAuthClient {
|
|
57
|
+
private readonly baseUrl;
|
|
58
|
+
private readonly fetcher;
|
|
59
|
+
private readonly credentials;
|
|
60
|
+
constructor(options: CanonicalAuthClientOptions);
|
|
61
|
+
private request;
|
|
62
|
+
signInEmail(email: string, password: string): Promise<AuthResult>;
|
|
63
|
+
signUpEmail(email: string, password: string, name: string): Promise<AuthResult>;
|
|
64
|
+
signInGoogle(callbackURL: string): Promise<AuthResult>;
|
|
65
|
+
requestGoogleDriveAccess(callbackURL?: string): Promise<AuthResult>;
|
|
66
|
+
getSession(cookie?: string): Promise<CanonicalAuthSession | null>;
|
|
67
|
+
signOut(): Promise<boolean>;
|
|
68
|
+
}
|
|
69
|
+
declare function createCanonicalAuthClient(baseUrl: string, fetcher?: typeof fetch): CanonicalAuthClient;
|
|
70
|
+
|
|
71
|
+
interface SystemUser extends CanonicalAuthUser {
|
|
72
|
+
username: string;
|
|
39
73
|
role: string;
|
|
40
74
|
status: string;
|
|
41
|
-
appProfiles?: Record<string,
|
|
75
|
+
appProfiles?: Record<string, unknown>;
|
|
76
|
+
}
|
|
77
|
+
interface AuthEnvironment {
|
|
78
|
+
SRKL_AUTH_BASE_URL?: string;
|
|
79
|
+
[key: string]: unknown;
|
|
42
80
|
}
|
|
43
|
-
declare function loginSystemUser(email: string, rawPass: string, isAdmin: boolean,
|
|
81
|
+
declare function loginSystemUser(email: string, rawPass: string, isAdmin: boolean, _queryDb: (sql: string, params?: unknown[]) => Promise<{
|
|
44
82
|
rows: any[];
|
|
45
|
-
}
|
|
83
|
+
}>, env?: AuthEnvironment): Promise<{
|
|
46
84
|
success: boolean;
|
|
47
85
|
status: number;
|
|
48
86
|
token?: string;
|
|
49
87
|
user?: SystemUser;
|
|
50
88
|
error?: string;
|
|
51
89
|
}>;
|
|
52
|
-
declare function resolveUserFromToken(tokenOrUserId: string,
|
|
90
|
+
declare function resolveUserFromToken(tokenOrUserId: string, _queryDb: (sql: string, params?: unknown[]) => Promise<{
|
|
53
91
|
rows: any[];
|
|
54
|
-
}
|
|
92
|
+
}>, env?: AuthEnvironment): Promise<SystemUser | null>;
|
|
55
93
|
|
|
56
|
-
export { AdminGuard, AuthSession, BaseAccountProvider, CreateUserData, LocalAccountProvider, PasswordHasher, type SystemUser, User, loginSystemUser, resolveUserFromToken };
|
|
94
|
+
export { AdminGuard, type AuthEnvironment, type AuthResult, AuthSession, BaseAccountProvider, CanonicalAuthClient, type CanonicalAuthClientOptions, type CanonicalAuthSession, type CanonicalAuthUser, CreateUserData, LocalAccountProvider, PasswordHasher, type SystemUser, User, createCanonicalAuthClient, loginSystemUser, resolveUserFromToken };
|
|
@@ -3,20 +3,24 @@ import {
|
|
|
3
3
|
BaseAccountProvider,
|
|
4
4
|
LocalAccountProvider,
|
|
5
5
|
PasswordHasher
|
|
6
|
-
} from "../chunk-
|
|
6
|
+
} from "../chunk-HT2SEDHD.js";
|
|
7
7
|
import {
|
|
8
|
+
CanonicalAuthClient,
|
|
8
9
|
RateLimiter,
|
|
9
10
|
SessionManager,
|
|
11
|
+
createCanonicalAuthClient,
|
|
10
12
|
loginSystemUser,
|
|
11
13
|
resolveUserFromToken
|
|
12
|
-
} from "../chunk-
|
|
14
|
+
} from "../chunk-M4GSR46Y.js";
|
|
13
15
|
export {
|
|
14
16
|
AdminGuard,
|
|
15
17
|
BaseAccountProvider,
|
|
18
|
+
CanonicalAuthClient,
|
|
16
19
|
LocalAccountProvider,
|
|
17
20
|
PasswordHasher,
|
|
18
21
|
RateLimiter,
|
|
19
22
|
SessionManager,
|
|
23
|
+
createCanonicalAuthClient,
|
|
20
24
|
loginSystemUser,
|
|
21
25
|
resolveUserFromToken
|
|
22
26
|
};
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import {
|
|
2
|
-
StorageAdapterFactory
|
|
3
|
-
} from "./chunk-R45CEV46.js";
|
|
4
1
|
import {
|
|
5
2
|
findUserWorkspace
|
|
6
3
|
} from "./chunk-6K75XXFD.js";
|
|
4
|
+
import {
|
|
5
|
+
StorageAdapterFactory
|
|
6
|
+
} from "./chunk-SHZAQTIT.js";
|
|
7
7
|
|
|
8
8
|
// src/services/workspaceGraphService.ts
|
|
9
9
|
function slugify(name) {
|
|
@@ -1,16 +1,10 @@
|
|
|
1
|
-
import {
|
|
2
|
-
workspaceGraphService
|
|
3
|
-
} from "./chunk-F6IDQLC2.js";
|
|
4
1
|
import {
|
|
5
2
|
getSrklAccounts,
|
|
6
3
|
srklListBackblazeBuckets
|
|
7
4
|
} from "./chunk-XHADDA75.js";
|
|
8
5
|
import {
|
|
9
|
-
|
|
10
|
-
} from "./chunk-
|
|
11
|
-
import {
|
|
12
|
-
b2ListBuckets
|
|
13
|
-
} from "./chunk-4QEMALXF.js";
|
|
6
|
+
workspaceGraphService
|
|
7
|
+
} from "./chunk-5RXXC6GE.js";
|
|
14
8
|
import {
|
|
15
9
|
formatCanonicalWorkspaceFolderName,
|
|
16
10
|
sanitizeWorkspaceName
|
|
@@ -25,12 +19,18 @@ import {
|
|
|
25
19
|
import {
|
|
26
20
|
queryUnifiedDb
|
|
27
21
|
} from "./chunk-VAFCR2MS.js";
|
|
22
|
+
import {
|
|
23
|
+
b2ListBuckets
|
|
24
|
+
} from "./chunk-4QEMALXF.js";
|
|
25
|
+
import {
|
|
26
|
+
StorageAdapterFactory
|
|
27
|
+
} from "./chunk-SHZAQTIT.js";
|
|
28
28
|
import {
|
|
29
29
|
RateLimiter,
|
|
30
30
|
SessionManager,
|
|
31
31
|
loginSystemUser,
|
|
32
32
|
resolveUserFromToken
|
|
33
|
-
} from "./chunk-
|
|
33
|
+
} from "./chunk-M4GSR46Y.js";
|
|
34
34
|
import {
|
|
35
35
|
createService,
|
|
36
36
|
createServiceDefinition
|
|
@@ -56,12 +56,12 @@ var AuthServiceDefinition = createServiceDefinition({
|
|
|
56
56
|
login: {
|
|
57
57
|
id: "AuthService.login",
|
|
58
58
|
name: "login",
|
|
59
|
-
handler: async (input) => loginSystemUser(input.email, input.password, Boolean(input.isAdmin), (sql, params) => queryUnifiedDb(sql, params, input.env))
|
|
59
|
+
handler: async (input) => loginSystemUser(input.email, input.password, Boolean(input.isAdmin), (sql, params) => queryUnifiedDb(sql, params, input.env), input.env)
|
|
60
60
|
},
|
|
61
61
|
resolveUser: {
|
|
62
62
|
id: "AuthService.resolveUser",
|
|
63
63
|
name: "resolveUser",
|
|
64
|
-
handler: async (input) => resolveUserFromToken(input.tokenOrUserId, (sql, params) => queryUnifiedDb(sql, params, input.env))
|
|
64
|
+
handler: async (input) => resolveUserFromToken(input.tokenOrUserId, (sql, params) => queryUnifiedDb(sql, params, input.env), input.env)
|
|
65
65
|
},
|
|
66
66
|
checkRateLimit: {
|
|
67
67
|
id: "AuthService.checkRateLimit",
|
|
@@ -11,6 +11,9 @@ import {
|
|
|
11
11
|
b2UploadFile,
|
|
12
12
|
queryUnifiedDb
|
|
13
13
|
} from "./chunk-VAFCR2MS.js";
|
|
14
|
+
import {
|
|
15
|
+
GoogleDriveAdapter
|
|
16
|
+
} from "./chunk-KKRWZDMN.js";
|
|
14
17
|
|
|
15
18
|
// src/workspace/WorkspaceFS.ts
|
|
16
19
|
import { SkipoBase } from "@ultrst-armada/kloks/functions";
|
|
@@ -374,6 +377,86 @@ async function syncWorkspace(workspaceId) {
|
|
|
374
377
|
};
|
|
375
378
|
}
|
|
376
379
|
}
|
|
380
|
+
const isGoogleDrive = workspace.storageProvider === "google_drive" || workspace.storageConfig?.type === "google_drive";
|
|
381
|
+
if (isGoogleDrive) {
|
|
382
|
+
const creds = workspace.storageConfig || connection?.credentials || connection || {};
|
|
383
|
+
const rootFolderId = creds.googleRootFolderId || creds.rootFolderId || workspace.googleRootFolderId;
|
|
384
|
+
const adapter = new GoogleDriveAdapter({
|
|
385
|
+
provider: "google_drive",
|
|
386
|
+
googleAccessToken: creds.googleAccessToken || creds.accessToken,
|
|
387
|
+
googleAccessTokenExpiresAt: creds.googleAccessTokenExpiresAt,
|
|
388
|
+
googleRefreshToken: creds.googleRefreshToken || creds.refreshToken,
|
|
389
|
+
googleClientId: creds.googleClientId || process.env.GOOGLE_CLIENT_ID,
|
|
390
|
+
googleRootFolderId: rootFolderId
|
|
391
|
+
});
|
|
392
|
+
try {
|
|
393
|
+
const remoteFiles = await adapter.list("");
|
|
394
|
+
const remoteMap = /* @__PURE__ */ new Map();
|
|
395
|
+
for (const rf of remoteFiles) {
|
|
396
|
+
if (rf.path) remoteMap.set(rf.path, { size: rf.size, timestamp: rf.modifiedAt });
|
|
397
|
+
}
|
|
398
|
+
const localFiles = await collectLocalFiles(workingDir);
|
|
399
|
+
const localMap = /* @__PURE__ */ new Map();
|
|
400
|
+
for (const lf of localFiles) {
|
|
401
|
+
localMap.set(lf.relPath, lf);
|
|
402
|
+
}
|
|
403
|
+
for (const [relPath, rInfo] of remoteMap.entries()) {
|
|
404
|
+
const lFile = localMap.get(relPath);
|
|
405
|
+
const shouldDownload = !lFile || rInfo.timestamp > lFile.mtimeMs + 2e3;
|
|
406
|
+
if (shouldDownload) {
|
|
407
|
+
const buf = await adapter.read(relPath);
|
|
408
|
+
const destPath = path.join(workingDir, relPath);
|
|
409
|
+
await fs.promises.mkdir(path.dirname(destPath), { recursive: true });
|
|
410
|
+
await fs.promises.writeFile(destPath, buf);
|
|
411
|
+
try {
|
|
412
|
+
const fileTime = new Date(rInfo.timestamp);
|
|
413
|
+
await fs.promises.utimes(destPath, fileTime, fileTime);
|
|
414
|
+
} catch {
|
|
415
|
+
}
|
|
416
|
+
syncedFiles.push({ fileName: relPath, action: "DOWNLOADED", sizeBytes: buf.byteLength });
|
|
417
|
+
downloadedCount++;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
for (const [relPath, lFile] of localMap.entries()) {
|
|
421
|
+
const rInfo = remoteMap.get(relPath);
|
|
422
|
+
const shouldUpload = !rInfo || lFile.mtimeMs > rInfo.timestamp + 2e3;
|
|
423
|
+
if (shouldUpload) {
|
|
424
|
+
const content = await fs.promises.readFile(lFile.fullPath);
|
|
425
|
+
await adapter.write(relPath, new Uint8Array(content));
|
|
426
|
+
syncedFiles.push({ fileName: relPath, action: "UPLOADED", sizeBytes: lFile.sizeBytes });
|
|
427
|
+
uploadedCount++;
|
|
428
|
+
} else if (!syncedFiles.some((f) => f.fileName === relPath)) {
|
|
429
|
+
syncedFiles.push({ fileName: relPath, action: "UP_TO_DATE", sizeBytes: lFile.sizeBytes });
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return {
|
|
433
|
+
success: true,
|
|
434
|
+
workspaceId,
|
|
435
|
+
folderKey,
|
|
436
|
+
workingDir,
|
|
437
|
+
persistenceType: "google_drive",
|
|
438
|
+
targetLocation: `GoogleDrive://${rootFolderId || "root"}`,
|
|
439
|
+
syncedFiles,
|
|
440
|
+
uploadedCount,
|
|
441
|
+
downloadedCount,
|
|
442
|
+
syncedAt: nowIso
|
|
443
|
+
};
|
|
444
|
+
} catch (err) {
|
|
445
|
+
return {
|
|
446
|
+
success: false,
|
|
447
|
+
workspaceId,
|
|
448
|
+
folderKey,
|
|
449
|
+
workingDir,
|
|
450
|
+
persistenceType: "google_drive",
|
|
451
|
+
targetLocation: `GoogleDrive://${rootFolderId || "root"}`,
|
|
452
|
+
syncedFiles,
|
|
453
|
+
uploadedCount,
|
|
454
|
+
downloadedCount,
|
|
455
|
+
syncedAt: nowIso,
|
|
456
|
+
error: err.message || "Google Drive sync failed."
|
|
457
|
+
};
|
|
458
|
+
}
|
|
459
|
+
}
|
|
377
460
|
const vaultDir = path.resolve(process.cwd(), folderKey);
|
|
378
461
|
await fs.promises.mkdir(vaultDir, { recursive: true });
|
|
379
462
|
try {
|
|
@@ -490,7 +573,7 @@ var WorkingWorkspaceService = class {
|
|
|
490
573
|
}
|
|
491
574
|
async deleteFile(_workspaceId, filePath) {
|
|
492
575
|
const cleanRel = filePath.replace(/^\/+/, "");
|
|
493
|
-
const fullPath = path2.resolve(this.getWorkingDir(), cleanRel);
|
|
576
|
+
const fullPath = path2.resolve(this.getWorkingDir(_workspaceId), cleanRel);
|
|
494
577
|
try {
|
|
495
578
|
await fs2.promises.unlink(fullPath);
|
|
496
579
|
return { success: true };
|
|
@@ -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
|
+
};
|