@lacrous/omnicloud 0.2.7 → 0.2.9
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/cli.js +8 -1
- package/dist/index.cjs +181 -7
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +53 -0
- package/dist/index.d.ts +53 -0
- package/dist/index.js +181 -7
- package/dist/index.js.map +1 -1
- package/package.json +4 -4
- package/server/index.js +220 -10
- package/server/web/assets/index-BjgDY1Om.js +346 -0
- package/server/web/assets/index-DZrY56Sp.css +1 -0
- package/server/web/index.html +2 -2
- package/server/web/assets/index-Caa-mAUw.js +0 -339
- package/server/web/assets/index-kc0MNEkT.css +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lacrous/omnicloud",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.9",
|
|
4
4
|
"description": "SDK and core building blocks for OmniCloud — self-hosted cloud storage powered by your Telegram account.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -48,9 +48,9 @@
|
|
|
48
48
|
"prisma": "^6.2.0"
|
|
49
49
|
},
|
|
50
50
|
"devDependencies": {
|
|
51
|
-
"@omnicloud/core": "0.2.
|
|
52
|
-
"@omnicloud/shared": "0.2.
|
|
53
|
-
"@omnicloud/telegram": "0.2.
|
|
51
|
+
"@omnicloud/core": "0.2.9",
|
|
52
|
+
"@omnicloud/shared": "0.2.9",
|
|
53
|
+
"@omnicloud/telegram": "0.2.9",
|
|
54
54
|
"@types/node": "^22.10.0",
|
|
55
55
|
"esbuild": "0.25.12",
|
|
56
56
|
"tsup": "^8.3.0",
|
package/server/index.js
CHANGED
|
@@ -2912,10 +2912,13 @@ var TelegramStorageProvider = class {
|
|
|
2912
2912
|
};
|
|
2913
2913
|
|
|
2914
2914
|
// ../telegram/src/connection.ts
|
|
2915
|
+
import { randomUUID } from "node:crypto";
|
|
2915
2916
|
import { Api as Api3 } from "telegram";
|
|
2916
2917
|
import { computeCheck } from "telegram/Password.js";
|
|
2917
2918
|
var TELEGRAM_PROVIDER = "telegram";
|
|
2918
2919
|
var LOGIN_FLOW_TTL_MS = 5 * 60 * 1e3;
|
|
2920
|
+
var QR_FLOW_TTL_MS = 5 * 60 * 1e3;
|
|
2921
|
+
var QR_APPROVAL_POLL_MS = 2e3;
|
|
2919
2922
|
var TelegramConnectionService = class {
|
|
2920
2923
|
constructor(repos, clientManager, credentials) {
|
|
2921
2924
|
this.repos = repos;
|
|
@@ -2923,6 +2926,7 @@ var TelegramConnectionService = class {
|
|
|
2923
2926
|
this.credentials = credentials;
|
|
2924
2927
|
}
|
|
2925
2928
|
flows = /* @__PURE__ */ new Map();
|
|
2929
|
+
qrFlows = /* @__PURE__ */ new Map();
|
|
2926
2930
|
/** Sends the Telegram confirmation code for the phone number. */
|
|
2927
2931
|
async startLogin(phone) {
|
|
2928
2932
|
this.sweepExpiredFlows();
|
|
@@ -2971,6 +2975,167 @@ var TelegramConnectionService = class {
|
|
|
2971
2975
|
}
|
|
2972
2976
|
return this.finalizeLogin(flow);
|
|
2973
2977
|
}
|
|
2978
|
+
/**
|
|
2979
|
+
* Starts a QR sign-in. Returns a flow id and the first login token. The phone
|
|
2980
|
+
* approves the token in the Telegram app; `pollQrLogin` reports the result.
|
|
2981
|
+
*/
|
|
2982
|
+
async startQrLogin() {
|
|
2983
|
+
this.sweepExpiredQrFlows();
|
|
2984
|
+
const client = newTelegramClient(this.credentials);
|
|
2985
|
+
try {
|
|
2986
|
+
await client.connect();
|
|
2987
|
+
const flow = {
|
|
2988
|
+
client,
|
|
2989
|
+
createdAt: Date.now(),
|
|
2990
|
+
approved: false,
|
|
2991
|
+
needsPassword: false,
|
|
2992
|
+
failure: null
|
|
2993
|
+
};
|
|
2994
|
+
const flowId = randomUUID();
|
|
2995
|
+
const token = await this.exportQrToken(client);
|
|
2996
|
+
this.qrFlows.set(flowId, flow);
|
|
2997
|
+
this.watchQrApproval(flowId, flow);
|
|
2998
|
+
return { flowId, token };
|
|
2999
|
+
} catch (error) {
|
|
3000
|
+
await this.disconnectQuietly(client);
|
|
3001
|
+
throw mapTelegramError(error, "Could not start Telegram QR sign-in");
|
|
3002
|
+
}
|
|
3003
|
+
}
|
|
3004
|
+
/**
|
|
3005
|
+
* Reports where a QR sign-in stands. While waiting, returns a fresh token
|
|
3006
|
+
* (Telegram rotates them every few seconds). On approval, completes the login.
|
|
3007
|
+
*/
|
|
3008
|
+
async pollQrLogin(flowId) {
|
|
3009
|
+
const flow = this.requireQrFlow(flowId);
|
|
3010
|
+
if (flow.failure) {
|
|
3011
|
+
this.qrFlows.delete(flowId);
|
|
3012
|
+
await this.disconnectQuietly(flow.client);
|
|
3013
|
+
throw new TelegramConnectionError(flow.failure);
|
|
3014
|
+
}
|
|
3015
|
+
if (flow.needsPassword) return { status: "password_required" };
|
|
3016
|
+
if (!flow.approved) {
|
|
3017
|
+
const token = await this.exportQrToken(flow.client).catch((error) => {
|
|
3018
|
+
this.qrFlows.delete(flowId);
|
|
3019
|
+
void this.disconnectQuietly(flow.client);
|
|
3020
|
+
throw mapTelegramError(error, "Could not refresh the Telegram QR code");
|
|
3021
|
+
});
|
|
3022
|
+
return { status: "waiting", token };
|
|
3023
|
+
}
|
|
3024
|
+
return this.completeQrLogin(flowId, flow);
|
|
3025
|
+
}
|
|
3026
|
+
/** Completes a QR sign-in for an account with two-factor authentication. */
|
|
3027
|
+
async submitQrPassword(flowId, password) {
|
|
3028
|
+
const flow = this.requireQrFlow(flowId);
|
|
3029
|
+
try {
|
|
3030
|
+
const passwordInfo = await flow.client.invoke(new Api3.account.GetPassword());
|
|
3031
|
+
const check = await computeCheck(passwordInfo, password);
|
|
3032
|
+
await flow.client.invoke(new Api3.auth.CheckPassword({ password: check }));
|
|
3033
|
+
} catch (error) {
|
|
3034
|
+
throw mapTelegramError(error, "Telegram sign-in failed");
|
|
3035
|
+
}
|
|
3036
|
+
return this.completeQrLogin(flowId, flow);
|
|
3037
|
+
}
|
|
3038
|
+
async exportQrToken(client) {
|
|
3039
|
+
const result = await client.invoke(
|
|
3040
|
+
new Api3.auth.ExportLoginToken({
|
|
3041
|
+
apiId: this.credentials.apiId,
|
|
3042
|
+
apiHash: this.credentials.apiHash,
|
|
3043
|
+
exceptIds: []
|
|
3044
|
+
})
|
|
3045
|
+
);
|
|
3046
|
+
if (!(result instanceof Api3.auth.LoginToken)) {
|
|
3047
|
+
throw new TelegramConnectionError("Telegram did not issue a QR login token");
|
|
3048
|
+
}
|
|
3049
|
+
const tokenBase64 = Buffer.from(result.token).toString("base64url");
|
|
3050
|
+
return {
|
|
3051
|
+
url: `tg://login?token=${tokenBase64}`,
|
|
3052
|
+
expiresAt: result.expires * 1e3
|
|
3053
|
+
};
|
|
3054
|
+
}
|
|
3055
|
+
/** Waits for the phone to approve the token, then marks the flow approved. */
|
|
3056
|
+
watchQrApproval(flowId, flow) {
|
|
3057
|
+
const timer = setInterval(() => {
|
|
3058
|
+
if (Date.now() - flow.createdAt > QR_FLOW_TTL_MS) {
|
|
3059
|
+
clearInterval(timer);
|
|
3060
|
+
this.qrFlows.delete(flowId);
|
|
3061
|
+
void this.disconnectQuietly(flow.client);
|
|
3062
|
+
return;
|
|
3063
|
+
}
|
|
3064
|
+
if (flow.approved || flow.needsPassword || flow.failure) {
|
|
3065
|
+
clearInterval(timer);
|
|
3066
|
+
return;
|
|
3067
|
+
}
|
|
3068
|
+
this.probeQrApproval(flow).then(
|
|
3069
|
+
(state) => {
|
|
3070
|
+
if (state === "approved") {
|
|
3071
|
+
flow.approved = true;
|
|
3072
|
+
clearInterval(timer);
|
|
3073
|
+
} else if (state === "password") {
|
|
3074
|
+
flow.needsPassword = true;
|
|
3075
|
+
clearInterval(timer);
|
|
3076
|
+
}
|
|
3077
|
+
},
|
|
3078
|
+
(error) => {
|
|
3079
|
+
flow.failure = mapTelegramError(error, "Telegram QR sign-in failed").message;
|
|
3080
|
+
clearInterval(timer);
|
|
3081
|
+
}
|
|
3082
|
+
);
|
|
3083
|
+
}, QR_APPROVAL_POLL_MS);
|
|
3084
|
+
timer.unref();
|
|
3085
|
+
}
|
|
3086
|
+
/**
|
|
3087
|
+
* Asks Telegram whether the token was approved. Approval is observed by
|
|
3088
|
+
* `ExportLoginToken` returning `LoginTokenSuccess` (or a DC migration).
|
|
3089
|
+
*/
|
|
3090
|
+
async probeQrApproval(flow) {
|
|
3091
|
+
try {
|
|
3092
|
+
const result = await flow.client.invoke(
|
|
3093
|
+
new Api3.auth.ExportLoginToken({
|
|
3094
|
+
apiId: this.credentials.apiId,
|
|
3095
|
+
apiHash: this.credentials.apiHash,
|
|
3096
|
+
exceptIds: []
|
|
3097
|
+
})
|
|
3098
|
+
);
|
|
3099
|
+
if (result instanceof Api3.auth.LoginTokenSuccess) return "approved";
|
|
3100
|
+
if (result instanceof Api3.auth.LoginTokenMigrateTo) {
|
|
3101
|
+
await flow.client._switchDC(result.dcId);
|
|
3102
|
+
const migrated = await flow.client.invoke(
|
|
3103
|
+
new Api3.auth.ImportLoginToken({ token: result.token })
|
|
3104
|
+
);
|
|
3105
|
+
if (migrated instanceof Api3.auth.LoginTokenSuccess) return "approved";
|
|
3106
|
+
}
|
|
3107
|
+
return "pending";
|
|
3108
|
+
} catch (error) {
|
|
3109
|
+
if (isPasswordRequiredError(error)) return "password";
|
|
3110
|
+
throw error;
|
|
3111
|
+
}
|
|
3112
|
+
}
|
|
3113
|
+
async completeQrLogin(flowId, flow) {
|
|
3114
|
+
this.qrFlows.delete(flowId);
|
|
3115
|
+
const user = await this.finalizeConnectedClient(flow.client, null);
|
|
3116
|
+
return { status: "approved", user };
|
|
3117
|
+
}
|
|
3118
|
+
requireQrFlow(flowId) {
|
|
3119
|
+
const flow = this.qrFlows.get(flowId);
|
|
3120
|
+
if (!flow) {
|
|
3121
|
+
throw new NotFoundError("This Telegram QR sign-in has ended. Start again.");
|
|
3122
|
+
}
|
|
3123
|
+
if (Date.now() - flow.createdAt > QR_FLOW_TTL_MS) {
|
|
3124
|
+
this.qrFlows.delete(flowId);
|
|
3125
|
+
void this.disconnectQuietly(flow.client);
|
|
3126
|
+
throw new ValidationError("The QR sign-in expired. Start again.");
|
|
3127
|
+
}
|
|
3128
|
+
return flow;
|
|
3129
|
+
}
|
|
3130
|
+
sweepExpiredQrFlows() {
|
|
3131
|
+
const now = Date.now();
|
|
3132
|
+
for (const [flowId, flow] of this.qrFlows) {
|
|
3133
|
+
if (now - flow.createdAt > QR_FLOW_TTL_MS) {
|
|
3134
|
+
this.qrFlows.delete(flowId);
|
|
3135
|
+
void this.disconnectQuietly(flow.client);
|
|
3136
|
+
}
|
|
3137
|
+
}
|
|
3138
|
+
}
|
|
2974
3139
|
/** Aborts a pending login flow (disconnects the client). */
|
|
2975
3140
|
async cancelLogin(phone) {
|
|
2976
3141
|
const flow = this.flows.get(phone);
|
|
@@ -3032,11 +3197,20 @@ var TelegramConnectionService = class {
|
|
|
3032
3197
|
}
|
|
3033
3198
|
async finalizeLogin(flow) {
|
|
3034
3199
|
this.flows.delete(flow.phone);
|
|
3200
|
+
const user = await this.finalizeConnectedClient(flow.client, flow.phone);
|
|
3201
|
+
return { status: "ok", user };
|
|
3202
|
+
}
|
|
3203
|
+
/**
|
|
3204
|
+
* Saves an authorized client's session, upserts the user and creates storage.
|
|
3205
|
+
* `phone` is the number the user typed; QR sign-in passes null and relies on
|
|
3206
|
+
* the number Telegram reports.
|
|
3207
|
+
*/
|
|
3208
|
+
async finalizeConnectedClient(client, phone) {
|
|
3035
3209
|
let stringSession;
|
|
3036
3210
|
let user;
|
|
3037
3211
|
try {
|
|
3038
|
-
stringSession =
|
|
3039
|
-
const me = await
|
|
3212
|
+
stringSession = client.session.save();
|
|
3213
|
+
const me = await client.getMe();
|
|
3040
3214
|
if (!(me instanceof Api3.User)) {
|
|
3041
3215
|
throw new Error("Telegram did not return the authenticated user");
|
|
3042
3216
|
}
|
|
@@ -3045,16 +3219,16 @@ var TelegramConnectionService = class {
|
|
|
3045
3219
|
username: me.username ?? null,
|
|
3046
3220
|
firstName: me.firstName ?? null,
|
|
3047
3221
|
lastName: me.lastName ?? null,
|
|
3048
|
-
phone: me.phone ??
|
|
3222
|
+
phone: me.phone ?? phone ?? ""
|
|
3049
3223
|
});
|
|
3050
3224
|
} catch (error) {
|
|
3051
|
-
await this.disconnectQuietly(
|
|
3225
|
+
await this.disconnectQuietly(client);
|
|
3052
3226
|
throw mapTelegramError(error, "Could not complete the Telegram connection");
|
|
3053
3227
|
}
|
|
3054
3228
|
await this.repos.sessions.save(user.id, stringSession);
|
|
3055
|
-
await this.ensureStorageForClient(user.id,
|
|
3056
|
-
this.clientManager.cacheClient(user.id,
|
|
3057
|
-
return
|
|
3229
|
+
await this.ensureStorageForClient(user.id, client);
|
|
3230
|
+
this.clientManager.cacheClient(user.id, client);
|
|
3231
|
+
return user;
|
|
3058
3232
|
}
|
|
3059
3233
|
sweepExpiredFlows() {
|
|
3060
3234
|
const now = Date.now();
|
|
@@ -3975,11 +4149,11 @@ var LOGGER_OPTIONS = (level) => ({
|
|
|
3975
4149
|
});
|
|
3976
4150
|
|
|
3977
4151
|
// ../../apps/api/src/http.ts
|
|
3978
|
-
import { randomUUID } from "node:crypto";
|
|
4152
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
3979
4153
|
function resolveRequestId(headers) {
|
|
3980
4154
|
const inbound = headers[REQUEST_ID_HEADER];
|
|
3981
4155
|
const candidate = Array.isArray(inbound) ? inbound[0] : inbound;
|
|
3982
|
-
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= 128 ? candidate :
|
|
4156
|
+
return typeof candidate === "string" && candidate.length > 0 && candidate.length <= 128 ? candidate : randomUUID2();
|
|
3983
4157
|
}
|
|
3984
4158
|
function attachRequestId(request, reply) {
|
|
3985
4159
|
reply.header(REQUEST_ID_HEADER, request.id);
|
|
@@ -4298,6 +4472,42 @@ function registerAuthRoutes(app2, container2) {
|
|
|
4298
4472
|
);
|
|
4299
4473
|
return reply.send({ status: "ok", user: toUserDTO(result.user) });
|
|
4300
4474
|
});
|
|
4475
|
+
app2.post("/api/auth/telegram/qr/start", async (request, reply) => {
|
|
4476
|
+
limiter(request.ip);
|
|
4477
|
+
const started = await container2.connection.startQrLogin();
|
|
4478
|
+
return reply.send({ flowId: started.flowId, token: started.token });
|
|
4479
|
+
});
|
|
4480
|
+
app2.get("/api/auth/telegram/qr/status", async (request, reply) => {
|
|
4481
|
+
limiter(request.ip);
|
|
4482
|
+
const flowId = requireString(request.query, "flowId");
|
|
4483
|
+
const result = await container2.connection.pollQrLogin(flowId);
|
|
4484
|
+
if (result.status === "approved") {
|
|
4485
|
+
await issueSessionCookie(
|
|
4486
|
+
container2.authSessions,
|
|
4487
|
+
request,
|
|
4488
|
+
reply,
|
|
4489
|
+
result.user.id,
|
|
4490
|
+
container2.config.cookieSecure
|
|
4491
|
+
);
|
|
4492
|
+
return reply.send({ status: "ok", user: toUserDTO(result.user) });
|
|
4493
|
+
}
|
|
4494
|
+
return reply.send(result);
|
|
4495
|
+
});
|
|
4496
|
+
app2.post("/api/auth/telegram/qr/password", async (request, reply) => {
|
|
4497
|
+
limiter(request.ip);
|
|
4498
|
+
const body = requireBody(request);
|
|
4499
|
+
const flowId = requireString(body, "flowId");
|
|
4500
|
+
const password = requireString(body, "password");
|
|
4501
|
+
const result = await container2.connection.submitQrPassword(flowId, password);
|
|
4502
|
+
await issueSessionCookie(
|
|
4503
|
+
container2.authSessions,
|
|
4504
|
+
request,
|
|
4505
|
+
reply,
|
|
4506
|
+
result.user.id,
|
|
4507
|
+
container2.config.cookieSecure
|
|
4508
|
+
);
|
|
4509
|
+
return reply.send({ status: "ok", user: toUserDTO(result.user) });
|
|
4510
|
+
});
|
|
4301
4511
|
app2.get("/api/auth/me", async (request) => {
|
|
4302
4512
|
const user = await currentUser(container2.repos, container2.authSessions, request.cookies);
|
|
4303
4513
|
if (!user) return { user: null, storage: null, health: null };
|
|
@@ -4685,7 +4895,7 @@ function encryptionCheck(configured) {
|
|
|
4685
4895
|
}
|
|
4686
4896
|
|
|
4687
4897
|
// ../../apps/api/src/routes/storage.ts
|
|
4688
|
-
var VERSION = "0.2.
|
|
4898
|
+
var VERSION = "0.2.9";
|
|
4689
4899
|
function registerStorageRoutes(app2, container2) {
|
|
4690
4900
|
app2.get("/api/health", async () => {
|
|
4691
4901
|
const database = await probeDatabase(container2);
|