@proton-app/main 0.1.7 → 0.1.8

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/index.d.ts CHANGED
@@ -4,3 +4,4 @@ export * from "./files";
4
4
  export * from "./communication";
5
5
  export * from "./setup";
6
6
  export * from "./storage";
7
+ export * from "./oauth";
package/dist/index.js CHANGED
@@ -21,3 +21,4 @@ __exportStar(require("./files"), exports);
21
21
  __exportStar(require("./communication"), exports);
22
22
  __exportStar(require("./setup"), exports);
23
23
  __exportStar(require("./storage"), exports);
24
+ __exportStar(require("./oauth"), exports);
@@ -0,0 +1,55 @@
1
+ import type { Storage, StorageSchema, SecretKey } from "./storage";
2
+ /**
3
+ * OAuth Options.
4
+ *
5
+ * @property name - Provider name shown in login messages.
6
+ * @property authorizeUrl - Provider's authorization page.
7
+ * @property tokenUrl - Provider's token endpoint.
8
+ * @property redirectUri - Loopback URI registered with the provider.
9
+ * @property clientId - App's client id.
10
+ * @property clientSecret - App's client secret, if the provider requires one.
11
+ * @property storage - App storage to keep tokens in.
12
+ * @property storageKey - Schema key for the encrypted tokens.
13
+ **/
14
+ export type OAuthOptions<T extends StorageSchema> = {
15
+ name: string;
16
+ authorizeUrl: string;
17
+ tokenUrl: string;
18
+ redirectUri: string;
19
+ clientId: string;
20
+ clientSecret?: string;
21
+ storage: Storage<T>;
22
+ storageKey: SecretKey<T>;
23
+ };
24
+ /**
25
+ * Creates an OAuth client for a desktop app.
26
+ *
27
+ * Logs in through the system browser using PKCE and a one-shot
28
+ * loopback server. Tokens are stored encrypted and refreshed
29
+ * automatically.
30
+ *
31
+ * @param opts - Provider and storage configuration.
32
+ * @returns Functions to connect, disconnect, and get access tokens.
33
+ **/
34
+ export declare function createOAuth<T extends StorageSchema>(opts: OAuthOptions<T>): {
35
+ /**
36
+ * Opens the browser login. Calling again while a login
37
+ * is pending reopens the same login page.
38
+ **/
39
+ connect(): Promise<void>;
40
+ /**
41
+ * Clears the stored tokens.
42
+ **/
43
+ disconnect(): Promise<void>;
44
+ /**
45
+ * Gets a valid access token, refreshing it if near expiry.
46
+ * A rejected refresh token clears the login.
47
+ *
48
+ * @returns The access token, or null when not connected.
49
+ **/
50
+ getAccessToken(): Promise<string | null>;
51
+ /**
52
+ * Whether tokens are stored.
53
+ **/
54
+ isConnected(): Promise<boolean>;
55
+ };
package/dist/oauth.js ADDED
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.createOAuth = createOAuth;
4
+ const http_1 = require("http");
5
+ const crypto_1 = require("crypto");
6
+ const electron_1 = require("electron");
7
+ const snatch_http_1 = require("snatch-http");
8
+ const EXPIRY_MARGIN = 60_000;
9
+ const LOGIN_TIMEOUT = 5 * 60_000;
10
+ /**
11
+ * Creates an OAuth client for a desktop app.
12
+ *
13
+ * Logs in through the system browser using PKCE and a one-shot
14
+ * loopback server. Tokens are stored encrypted and refreshed
15
+ * automatically.
16
+ *
17
+ * @param opts - Provider and storage configuration.
18
+ * @returns Functions to connect, disconnect, and get access tokens.
19
+ **/
20
+ function createOAuth(opts) {
21
+ const { name, authorizeUrl, tokenUrl, redirectUri, clientId, clientSecret, storage, storageKey } = opts;
22
+ const redirect = new URL(redirectUri);
23
+ let pendingConnect = null;
24
+ let refreshing = null;
25
+ let loginUrl = null;
26
+ const loadTokens = () => storage.getSecret(storageKey);
27
+ const saveTokens = (tokens) => storage.setSecret(storageKey, tokens);
28
+ async function requestToken(params) {
29
+ const data = await snatch_http_1.api.post({
30
+ url: tokenUrl,
31
+ headers: { accept: "application/json; charset=utf-8" },
32
+ data: new URLSearchParams({
33
+ client_id: clientId,
34
+ ...(clientSecret && { client_secret: clientSecret }),
35
+ ...params
36
+ })
37
+ });
38
+ const tokens = {
39
+ accessToken: data.access_token,
40
+ refreshToken: data.refresh_token,
41
+ expiresAt: Date.now() + data.expires_in * 1000
42
+ };
43
+ saveTokens(tokens); // refresh tokens are single-use: persist immediately
44
+ return tokens;
45
+ }
46
+ function listenForCode(state, onReady) {
47
+ return new Promise((resolve, reject) => {
48
+ const timer = setTimeout(() => done(new Error(`${name} login timed out.`)), LOGIN_TIMEOUT);
49
+ const server = (0, http_1.createServer)((req, res) => {
50
+ const url = new URL(req.url ?? "/", redirectUri);
51
+ if (url.pathname !== redirect.pathname)
52
+ return void res.writeHead(404).end();
53
+ const code = url.searchParams.get("code");
54
+ const valid = !!code && url.searchParams.get("state") === state;
55
+ res.writeHead(valid ? 200 : 400, { "Content-Type": "text/plain; charset=utf-8" });
56
+ res.end(valid
57
+ ? `Connected to ${name}. You can close this tab.`
58
+ : `${name} login failed. Try again from the app.`);
59
+ done(valid ? undefined : new Error(url.searchParams.get("error") ?? "Invalid callback."), code ?? undefined);
60
+ });
61
+ function done(err, code) {
62
+ clearTimeout(timer);
63
+ server.close();
64
+ err ? reject(err) : resolve(code);
65
+ }
66
+ server.once("error", err => done(err));
67
+ server.listen(Number(redirect.port), redirect.hostname, onReady);
68
+ });
69
+ }
70
+ async function runConnect() {
71
+ const verifier = (0, crypto_1.randomBytes)(32).toString("base64url");
72
+ const challenge = (0, crypto_1.createHash)("sha256").update(verifier).digest("base64url");
73
+ const state = (0, crypto_1.randomBytes)(16).toString("base64url");
74
+ const code = await listenForCode(state, () => {
75
+ const url = new URL(authorizeUrl);
76
+ url.search = new URLSearchParams({
77
+ client_id: clientId,
78
+ redirect_uri: redirectUri,
79
+ response_type: "code",
80
+ code_challenge: challenge,
81
+ code_challenge_method: "S256",
82
+ state
83
+ }).toString();
84
+ loginUrl = url.toString();
85
+ void electron_1.shell.openExternal(loginUrl);
86
+ });
87
+ await requestToken({
88
+ grant_type: "authorization_code",
89
+ redirect_uri: redirectUri,
90
+ code_verifier: verifier,
91
+ code
92
+ });
93
+ }
94
+ return {
95
+ /**
96
+ * Opens the browser login. Calling again while a login
97
+ * is pending reopens the same login page.
98
+ **/
99
+ connect() {
100
+ if (loginUrl)
101
+ void electron_1.shell.openExternal(loginUrl);
102
+ pendingConnect ??= runConnect().finally(() => {
103
+ pendingConnect = null;
104
+ loginUrl = null;
105
+ });
106
+ return pendingConnect;
107
+ },
108
+ /**
109
+ * Clears the stored tokens.
110
+ **/
111
+ async disconnect() {
112
+ saveTokens(null);
113
+ },
114
+ /**
115
+ * Gets a valid access token, refreshing it if near expiry.
116
+ * A rejected refresh token clears the login.
117
+ *
118
+ * @returns The access token, or null when not connected.
119
+ **/
120
+ async getAccessToken() {
121
+ const tokens = loadTokens();
122
+ if (!tokens)
123
+ return null;
124
+ if (Date.now() < tokens.expiresAt - EXPIRY_MARGIN)
125
+ return tokens.accessToken;
126
+ refreshing ??= requestToken({
127
+ grant_type: "refresh_token",
128
+ refresh_token: tokens.refreshToken
129
+ }).finally(() => (refreshing = null));
130
+ try {
131
+ return (await refreshing).accessToken;
132
+ }
133
+ catch (err) {
134
+ if (err instanceof snatch_http_1.ApiError && err.message === "invalid_grant")
135
+ saveTokens(null);
136
+ throw err;
137
+ }
138
+ },
139
+ /**
140
+ * Whether tokens are stored.
141
+ **/
142
+ async isConnected() {
143
+ return loadTokens() !== null;
144
+ }
145
+ };
146
+ }
package/dist/storage.d.ts CHANGED
@@ -1,12 +1,9 @@
1
1
  import ElectronStore from "electron-store";
2
2
  type JSONType = "string" | "number" | "integer" | "boolean" | "object" | "array" | "null";
3
- type StorageSchema = Record<string, {
3
+ export type StorageSchema = Record<string, {
4
4
  type: JSONType | readonly JSONType[];
5
5
  default: unknown;
6
6
  }>;
7
- type BaseStorage = {
8
- access_token: string | null;
9
- };
10
7
  type TypeMap = {
11
8
  string: string;
12
9
  number: number;
@@ -17,10 +14,27 @@ type TypeMap = {
17
14
  null: null;
18
15
  };
19
16
  type FromType<T> = T extends readonly JSONType[] ? TypeMap[T[number]] : T extends JSONType ? TypeMap[T] : never;
20
- type InferSchema<T extends StorageSchema> = BaseStorage & {
21
- [K in Exclude<keyof T, keyof BaseStorage>]: FromType<T[K]["type"]>;
17
+ type InferSchema<T extends StorageSchema> = {
18
+ [K in keyof T]: FromType<T[K]["type"]>;
22
19
  };
20
+ export type SecretKey<T extends StorageSchema> = {
21
+ [K in keyof T & string]: string extends InferSchema<T>[K] ? K : never;
22
+ }[keyof T & string];
23
23
  export declare class Storage<const T extends StorageSchema> extends ElectronStore<InferSchema<T>> {
24
24
  constructor(schema: T);
25
+ /**
26
+ * Reads an encrypted value.
27
+ *
28
+ * @param key - Schema key holding the secret.
29
+ * @returns The decrypted value, or null if unset or unreadable.
30
+ **/
31
+ getSecret<V>(key: SecretKey<T>): V | null;
32
+ /**
33
+ * Encrypts and stores a value.
34
+ *
35
+ * @param key - Schema key holding the secret.
36
+ * @param value - Value to store, or null to clear it.
37
+ **/
38
+ setSecret<V>(key: SecretKey<T>, value: V | null): void;
25
39
  }
26
40
  export {};
package/dist/storage.js CHANGED
@@ -5,17 +5,39 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
6
  exports.Storage = void 0;
7
7
  const electron_store_1 = __importDefault(require("electron-store"));
8
+ const electron_1 = require("electron");
8
9
  class Storage extends electron_store_1.default {
9
10
  constructor(schema) {
10
- super({
11
- schema: {
12
- access_token: {
13
- type: ["string", "null"],
14
- default: null
15
- },
16
- ...schema
17
- }
18
- });
11
+ super({ schema });
12
+ }
13
+ /**
14
+ * Reads an encrypted value.
15
+ *
16
+ * @param key - Schema key holding the secret.
17
+ * @returns The decrypted value, or null if unset or unreadable.
18
+ **/
19
+ getSecret(key) {
20
+ const raw = this.get(key);
21
+ if (typeof raw !== "string")
22
+ return null;
23
+ try {
24
+ return JSON.parse(electron_1.safeStorage.decryptString(Buffer.from(raw, "base64")));
25
+ }
26
+ catch {
27
+ return null;
28
+ }
29
+ }
30
+ /**
31
+ * Encrypts and stores a value.
32
+ *
33
+ * @param key - Schema key holding the secret.
34
+ * @param value - Value to store, or null to clear it.
35
+ **/
36
+ setSecret(key, value) {
37
+ const raw = value === null
38
+ ? null
39
+ : electron_1.safeStorage.encryptString(JSON.stringify(value)).toString("base64");
40
+ this.set(key, raw);
19
41
  }
20
42
  }
21
43
  exports.Storage = Storage;
package/package.json CHANGED
@@ -1,43 +1,76 @@
1
1
  {
2
-
3
2
  "name": "@proton-app/main",
4
- "version": "0.1.7",
3
+ "version": "0.1.8",
4
+
5
5
  "description": "",
6
+
6
7
  "keywords": [],
8
+
7
9
  "author": "",
10
+
8
11
  "license": "ISC",
12
+
9
13
  "main": "./dist/index.js",
14
+
10
15
  "types": "./dist/index.d.ts",
16
+
11
17
  "files": [
18
+
12
19
  "dist"
20
+
13
21
  ],
22
+
14
23
  "publishConfig": {
24
+
15
25
  "access": "public"
26
+
16
27
  },
28
+
17
29
  "exports": {
30
+
18
31
  ".": {
32
+
19
33
  "default": "./dist/index.js",
34
+
20
35
  "types": "./dist/index.d.ts"
36
+
21
37
  },
38
+
22
39
  "./types": "./dist/index.d.ts"
40
+
23
41
  },
42
+
24
43
  "engines": {},
25
44
 
26
45
  "scripts": {
46
+
27
47
  "build": "tsc"
48
+
28
49
  },
29
50
 
30
51
  "dependencies": {
52
+
31
53
  "@bouko/ts": "^0.3.8",
54
+
32
55
  "electron-store": "^11.0.2",
33
- "path": "^0.12.7"
56
+
57
+ "path": "^0.12.7",
58
+
59
+ "snatch-http": "^0.0.7"
60
+
34
61
  },
35
62
 
36
63
  "devDependencies": {
64
+
37
65
  "@types/react": "^19.2.13",
66
+
38
67
  "electron": "^39.2.7",
68
+
39
69
  "react-router-dom": "^7.12.0",
70
+
40
71
  "typescript": "^5.9.3"
72
+
41
73
  }
42
74
 
43
- }
75
+ }
76
+