@suronai/sdk 0.1.36 → 0.1.37
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/package.json +20 -28
- package/src/decrypt.js +12 -27
- package/src/vault.js +48 -100
- package/README.md +0 -131
- package/index.d.ts +0 -37
- package/src/poll.js +0 -55
package/package.json
CHANGED
|
@@ -1,28 +1,20 @@
|
|
|
1
|
-
{
|
|
2
|
-
"name": "@suronai/sdk",
|
|
3
|
-
"version": "0.1.
|
|
4
|
-
"description": "
|
|
5
|
-
"type": "module",
|
|
6
|
-
"main": "./src/index.js",
|
|
7
|
-
"
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
},
|
|
22
|
-
"dependencies": {
|
|
23
|
-
"@dotenvx/dotenvx": "latest"
|
|
24
|
-
},
|
|
25
|
-
"engines": {
|
|
26
|
-
"node": ">=18.0.0"
|
|
27
|
-
}
|
|
28
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"name": "@suronai/sdk",
|
|
3
|
+
"version": "0.1.37",
|
|
4
|
+
"description": "Suron SDK — boot-gated secrets delivery",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./src/index.js",
|
|
7
|
+
"exports": {
|
|
8
|
+
".": "./src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src"
|
|
12
|
+
],
|
|
13
|
+
"scripts": {
|
|
14
|
+
"test": "node src/index.js"
|
|
15
|
+
},
|
|
16
|
+
"engines": {
|
|
17
|
+
"node": ">=18.0.0"
|
|
18
|
+
},
|
|
19
|
+
"dependencies": {}
|
|
20
|
+
}
|
package/src/decrypt.js
CHANGED
|
@@ -1,29 +1,14 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
* which is explicit and race-condition-free
|
|
14
|
-
*
|
|
15
|
-
* @param {string} privateKey - 64-char hex secp256k1 private key from Suron
|
|
16
|
-
* @param {string} envDir - Directory containing the encrypted .env file
|
|
17
|
-
*/
|
|
18
|
-
export function decryptEnv(privateKey, envDir = process.cwd()) {
|
|
19
|
-
const envPath = join(envDir, ".env");
|
|
20
|
-
const src = readFileSync(envPath, "utf-8");
|
|
21
|
-
const parsed = dotenvxParse(src, { privateKey });
|
|
22
|
-
for (const [key, value] of Object.entries(parsed)) {
|
|
23
|
-
// Skip dotenvx's own metadata keys — these are infrastructure, not app secrets.
|
|
24
|
-
// DOTENV_PUBLIC_KEY is stored in plaintext in the .env file by dotenvx and
|
|
25
|
-
// would otherwise pollute the app's runtime environment.
|
|
26
|
-
if (key.startsWith("DOTENV_PUBLIC_KEY") || key.startsWith("DOTENV_PRIVATE_KEY")) continue;
|
|
27
|
-
process.env[key] = value;
|
|
1
|
+
export function parseEnv(plaintext) {
|
|
2
|
+
for (const line of plaintext.split("\n")) {
|
|
3
|
+
const trimmed = line.trim();
|
|
4
|
+
if (!trimmed || trimmed.startsWith("#")) continue;
|
|
5
|
+
const eq = trimmed.indexOf("=");
|
|
6
|
+
if (eq === -1) continue;
|
|
7
|
+
const key = trimmed.slice(0, eq).trim();
|
|
8
|
+
let val = trimmed.slice(eq + 1).trim();
|
|
9
|
+
if ((val.startsWith('"') && val.endsWith('"')) || (val.startsWith("'") && val.endsWith("'"))) {
|
|
10
|
+
val = val.slice(1, -1);
|
|
11
|
+
}
|
|
12
|
+
if (key) process.env[key] = val;
|
|
28
13
|
}
|
|
29
14
|
}
|
package/src/vault.js
CHANGED
|
@@ -1,104 +1,74 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "fs";
|
|
2
2
|
import { join } from "path";
|
|
3
3
|
import { SuronConfigError, SuronAppNotFoundError, SuronRateLimitError, SuronDeniedError, SuronTimeoutError } from "./errors.js";
|
|
4
|
-
import {
|
|
5
|
-
import { decryptEnv } from "./decrypt.js";
|
|
4
|
+
import { parseEnv } from "./decrypt.js";
|
|
6
5
|
|
|
7
|
-
|
|
8
|
-
* @typedef {Object} VaultOptions
|
|
9
|
-
* @property {string} [configPath] - Dir containing .suron.json. Default: process.cwd()
|
|
10
|
-
* @property {number} [timeout] - Ms to wait for Telegram approval. Default: 300000
|
|
11
|
-
* @property {number} [pollInterval] - Ms between status polls. Default: 3000
|
|
12
|
-
*/
|
|
6
|
+
const BASE_URL = "https://suronai.com";
|
|
13
7
|
|
|
14
|
-
/**
|
|
15
|
-
* Reads and validates .suron.json from the given directory.
|
|
16
|
-
* @param {string} dir
|
|
17
|
-
* @returns {{ app: string, id: string, api_url?: string }}
|
|
18
|
-
*/
|
|
19
8
|
function readSuronJson(dir) {
|
|
20
9
|
const path = join(dir, ".suron.json");
|
|
21
|
-
if (!existsSync(path)) {
|
|
22
|
-
throw new SuronConfigError(`.suron.json not found in ${dir}. Run: suron init`);
|
|
23
|
-
}
|
|
24
|
-
let raw;
|
|
25
|
-
try {
|
|
26
|
-
raw = readFileSync(path, "utf-8");
|
|
27
|
-
} catch (err) {
|
|
28
|
-
throw new SuronConfigError(`.suron.json could not be read: ${err}`);
|
|
29
|
-
}
|
|
10
|
+
if (!existsSync(path)) throw new SuronConfigError(`.suron.json not found in ${dir}. Run: suron init`);
|
|
30
11
|
let parsed;
|
|
31
|
-
try {
|
|
32
|
-
parsed = JSON.parse(raw);
|
|
33
|
-
} catch {
|
|
12
|
+
try { parsed = JSON.parse(readFileSync(path, "utf-8")); } catch {
|
|
34
13
|
throw new SuronConfigError(".suron.json is malformed JSON");
|
|
35
14
|
}
|
|
36
|
-
const
|
|
37
|
-
const
|
|
38
|
-
if (!
|
|
39
|
-
|
|
40
|
-
}
|
|
41
|
-
parsed.app = app;
|
|
42
|
-
parsed.id = id;
|
|
43
|
-
return parsed;
|
|
15
|
+
const app_id = typeof parsed.app_id === "string" ? parsed.app_id.trim() : "";
|
|
16
|
+
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
|
17
|
+
if (!app_id || !version) throw new SuronConfigError(".suron.json is missing 'app_id' or 'version'");
|
|
18
|
+
return { app_id, version, app_name: parsed.app_name };
|
|
44
19
|
}
|
|
45
20
|
|
|
46
|
-
|
|
47
|
-
* Sends a boot request and returns the request_id.
|
|
48
|
-
* @param {string} apiUrl
|
|
49
|
-
* @param {string} appId
|
|
50
|
-
* @returns {Promise<string>}
|
|
51
|
-
*/
|
|
52
|
-
async function requestAccess(apiUrl, appId) {
|
|
21
|
+
async function requestAccess(app_id) {
|
|
53
22
|
let res;
|
|
54
23
|
try {
|
|
55
|
-
res = await fetch(`${
|
|
24
|
+
res = await fetch(`${BASE_URL}/request-access`, {
|
|
56
25
|
method: "POST",
|
|
57
26
|
headers: { "Content-Type": "application/json" },
|
|
58
|
-
body: JSON.stringify({ app_id
|
|
27
|
+
body: JSON.stringify({ app_id }),
|
|
59
28
|
});
|
|
60
29
|
} catch (err) {
|
|
61
|
-
throw new Error(`Could not reach Suron API
|
|
30
|
+
throw new Error(`Could not reach Suron API: ${err.message}`);
|
|
62
31
|
}
|
|
63
|
-
|
|
64
|
-
if (res.status === 404) throw new SuronAppNotFoundError("App not found in Suron. Run: suron init");
|
|
32
|
+
if (res.status === 404) throw new SuronAppNotFoundError("App not found. Run: suron init");
|
|
65
33
|
if (res.status === 429) throw new SuronRateLimitError("Rate limit exceeded — max 20 boot requests per app per hour.");
|
|
66
34
|
if (!res.ok) {
|
|
67
35
|
const text = await res.text().catch(() => "");
|
|
68
|
-
throw new Error(
|
|
36
|
+
throw new Error(`/request-access failed (${res.status}): ${text}`);
|
|
69
37
|
}
|
|
70
|
-
|
|
71
38
|
const { request_id } = await res.json();
|
|
72
39
|
return request_id;
|
|
73
40
|
}
|
|
74
41
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
42
|
+
async function pollUntilApproved(request_id, timeout, pollInterval) {
|
|
43
|
+
const deadline = Date.now() + timeout;
|
|
44
|
+
while (Date.now() < deadline) {
|
|
45
|
+
await new Promise(r => setTimeout(r, pollInterval));
|
|
46
|
+
let res;
|
|
47
|
+
try { res = await fetch(`${BASE_URL}/status?request_id=${encodeURIComponent(request_id)}`); } catch { continue; }
|
|
48
|
+
if (!res.ok) continue;
|
|
49
|
+
let status;
|
|
50
|
+
try { ({ status } = await res.json()); } catch { continue; }
|
|
51
|
+
if (status === "approved") return;
|
|
52
|
+
if (status === "denied") throw new SuronDeniedError("Boot denied.");
|
|
53
|
+
}
|
|
54
|
+
throw new SuronTimeoutError(`No approval received within ${timeout / 1000}s.`);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function fetchVersion(app_id, version, request_id) {
|
|
58
|
+
const res = await fetch(`${BASE_URL}/apps/${app_id}/versions/${version}`, {
|
|
59
|
+
headers: {
|
|
60
|
+
"Content-Type": "application/json",
|
|
61
|
+
"X-Request-Id": request_id,
|
|
62
|
+
},
|
|
86
63
|
});
|
|
87
64
|
if (!res.ok) {
|
|
88
65
|
const data = await res.json().catch(() => ({}));
|
|
89
|
-
throw new Error(
|
|
66
|
+
throw new Error(`/apps/${app_id}/versions/${version} failed (${res.status}): ${data.error ?? "unknown"}`);
|
|
90
67
|
}
|
|
91
|
-
const {
|
|
92
|
-
return
|
|
68
|
+
const { env_plaintext } = await res.json();
|
|
69
|
+
return env_plaintext;
|
|
93
70
|
}
|
|
94
71
|
|
|
95
|
-
/**
|
|
96
|
-
* Waits for Telegram approval, then decrypts .env into process.env.
|
|
97
|
-
* Runs silently — no stdout output under any circumstances.
|
|
98
|
-
* Must be awaited before any code that reads process.env.
|
|
99
|
-
* @param {VaultOptions} [options]
|
|
100
|
-
* @returns {Promise<void>}
|
|
101
|
-
*/
|
|
102
72
|
export async function vault(options = {}) {
|
|
103
73
|
const {
|
|
104
74
|
configPath = process.cwd(),
|
|
@@ -106,44 +76,22 @@ export async function vault(options = {}) {
|
|
|
106
76
|
pollInterval = 3_000,
|
|
107
77
|
} = options;
|
|
108
78
|
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
const requestId = await requestAccess(apiUrl, suronJson.id);
|
|
116
|
-
|
|
117
|
-
await pollUntilApproved(apiUrl, requestId, timeout, pollInterval);
|
|
118
|
-
|
|
119
|
-
let privateKey = await fetchKey(apiUrl, requestId);
|
|
120
|
-
decryptEnv(privateKey, configPath);
|
|
121
|
-
// JS strings are immutable — assigning "" does not overwrite heap memory.
|
|
122
|
-
// The reference is dropped here so the GC can reclaim it normally.
|
|
123
|
-
privateKey = ""; // drop reference
|
|
79
|
+
const { app_id, version } = readSuronJson(configPath);
|
|
80
|
+
const request_id = await requestAccess(app_id);
|
|
81
|
+
await pollUntilApproved(request_id, timeout, pollInterval);
|
|
82
|
+
const env_plaintext = await fetchVersion(app_id, version, request_id);
|
|
83
|
+
parseEnv(env_plaintext);
|
|
124
84
|
}
|
|
125
85
|
|
|
126
|
-
/**
|
|
127
|
-
* Drop-in safe wrapper around vault().
|
|
128
|
-
* Handles all known Suron errors (including SuronRateLimitError) with
|
|
129
|
-
* a console.error + process.exit(1). Unknown errors are re-thrown.
|
|
130
|
-
*
|
|
131
|
-
* Usage:
|
|
132
|
-
* import { config } from "@suronai/sdk";
|
|
133
|
-
* await config();
|
|
134
|
-
*
|
|
135
|
-
* @param {VaultOptions} [options]
|
|
136
|
-
* @returns {Promise<void>}
|
|
137
|
-
*/
|
|
138
86
|
export async function config(options = {}) {
|
|
139
87
|
try {
|
|
140
88
|
await vault(options);
|
|
141
89
|
} catch (err) {
|
|
142
|
-
if (err instanceof SuronDeniedError) {
|
|
143
|
-
if (err instanceof SuronTimeoutError) {
|
|
144
|
-
if (err instanceof SuronRateLimitError) {
|
|
145
|
-
if (err instanceof SuronConfigError) {
|
|
146
|
-
if (err instanceof SuronAppNotFoundError) {
|
|
90
|
+
if (err instanceof SuronDeniedError) { process.stderr.write("[suron] Boot denied.\n"); process.exit(1); }
|
|
91
|
+
if (err instanceof SuronTimeoutError) { process.stderr.write("[suron] Approval timed out.\n"); process.exit(1); }
|
|
92
|
+
if (err instanceof SuronRateLimitError) { process.stderr.write(`[suron] ${err.message}\n`); process.exit(1); }
|
|
93
|
+
if (err instanceof SuronConfigError) { process.stderr.write(`[suron] ${err.message}\n`); process.exit(1); }
|
|
94
|
+
if (err instanceof SuronAppNotFoundError) { process.stderr.write(`[suron] ${err.message}\n`); process.exit(1); }
|
|
147
95
|
throw err;
|
|
148
96
|
}
|
|
149
97
|
}
|
package/README.md
DELETED
|
@@ -1,131 +0,0 @@
|
|
|
1
|
-
# @suronai/sdk
|
|
2
|
-
|
|
3
|
-
Runtime SDK for [Suron](https://suronai.com) — a drop-in replacement for `dotenv` that gates every app boot behind a Telegram approval before secrets are decrypted into `process.env`.
|
|
4
|
-
|
|
5
|
-
## Install
|
|
6
|
-
|
|
7
|
-
```bash
|
|
8
|
-
npm install @suronai/sdk
|
|
9
|
-
```
|
|
10
|
-
|
|
11
|
-
Requires Node.js 18+. Run [`suron init`](https://www.npmjs.com/package/@suronai/cli) in your project first.
|
|
12
|
-
|
|
13
|
-
---
|
|
14
|
-
|
|
15
|
-
## Usage
|
|
16
|
-
|
|
17
|
-
Replace your `dotenv` bootstrap at the very top of your entry point, before any `process.env` access:
|
|
18
|
-
|
|
19
|
-
```js
|
|
20
|
-
// before
|
|
21
|
-
import { config } from 'dotenv'
|
|
22
|
-
config()
|
|
23
|
-
|
|
24
|
-
// after
|
|
25
|
-
import { config } from '@suronai/sdk'
|
|
26
|
-
await config()
|
|
27
|
-
```
|
|
28
|
-
|
|
29
|
-
That's it. When your app starts:
|
|
30
|
-
|
|
31
|
-
1. Suron sends a message to your Telegram bot with an **Approve / Deny** button
|
|
32
|
-
2. `await config()` blocks until you tap Approve
|
|
33
|
-
3. Secrets are decrypted into `process.env` — your app continues normally
|
|
34
|
-
4. If you tap Deny, or the timeout expires, the process exits with a clear message
|
|
35
|
-
|
|
36
|
-
`suron init` can patch this automatically — it detects `dotenv` in your entry point and offers to replace it interactively.
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## Requirements
|
|
41
|
-
|
|
42
|
-
- `.suron.json` in the working directory — created by `suron init`
|
|
43
|
-
- Encrypted `.env` in the working directory — created by `suron init`
|
|
44
|
-
- No extra environment variables needed — the API URL is stored in `.suron.json`
|
|
45
|
-
|
|
46
|
-
---
|
|
47
|
-
|
|
48
|
-
## API
|
|
49
|
-
|
|
50
|
-
### `config(options?)`
|
|
51
|
-
|
|
52
|
-
Drop-in safe wrapper. Handles all known Suron errors with `console.error` + `process.exit(1)`. Only unexpected errors are re-thrown.
|
|
53
|
-
|
|
54
|
-
```js
|
|
55
|
-
import { config } from '@suronai/sdk'
|
|
56
|
-
await config()
|
|
57
|
-
```
|
|
58
|
-
|
|
59
|
-
### `vault(options?)`
|
|
60
|
-
|
|
61
|
-
Same as `config()` but throws on all errors, giving you full control over error handling:
|
|
62
|
-
|
|
63
|
-
```js
|
|
64
|
-
import { vault, SuronDeniedError, SuronTimeoutError, SuronConfigError, SuronAppNotFoundError } from '@suronai/sdk'
|
|
65
|
-
|
|
66
|
-
try {
|
|
67
|
-
await vault()
|
|
68
|
-
} catch (err) {
|
|
69
|
-
if (err instanceof SuronDeniedError) { console.error('[suron] Boot denied.'); process.exit(1) }
|
|
70
|
-
if (err instanceof SuronTimeoutError) { console.error('[suron] Approval timed out.'); process.exit(1) }
|
|
71
|
-
if (err instanceof SuronConfigError) { console.error('[suron]', err.message); process.exit(1) }
|
|
72
|
-
if (err instanceof SuronAppNotFoundError) { console.error('[suron]', err.message); process.exit(1) }
|
|
73
|
-
throw err
|
|
74
|
-
}
|
|
75
|
-
```
|
|
76
|
-
|
|
77
|
-
### Options
|
|
78
|
-
|
|
79
|
-
Both `config()` and `vault()` accept the same options object:
|
|
80
|
-
|
|
81
|
-
```js
|
|
82
|
-
await config({
|
|
83
|
-
configPath: '/custom/path', // dir containing .suron.json — default: process.cwd()
|
|
84
|
-
timeout: 300_000, // ms to wait for Telegram approval — default: 5 minutes
|
|
85
|
-
pollInterval: 3_000, // ms between /status polls — default: 3s
|
|
86
|
-
})
|
|
87
|
-
```
|
|
88
|
-
|
|
89
|
-
---
|
|
90
|
-
|
|
91
|
-
## Errors
|
|
92
|
-
|
|
93
|
-
| Class | Thrown when |
|
|
94
|
-
|---|---|
|
|
95
|
-
| `SuronConfigError` | `.suron.json` missing, malformed, or `SURON_API_URL` not set |
|
|
96
|
-
| `SuronAppNotFoundError` | App not registered — run `suron init` |
|
|
97
|
-
| `SuronRateLimitError` | More than 20 boot requests in 1 hour |
|
|
98
|
-
| `SuronDeniedError` | You tapped Deny in Telegram |
|
|
99
|
-
| `SuronTimeoutError` | No approval received within the timeout |
|
|
100
|
-
|
|
101
|
-
`config()` exits the process for all errors except `SuronRateLimitError` and unknown errors, which are re-thrown. `vault()` always throws.
|
|
102
|
-
|
|
103
|
-
---
|
|
104
|
-
|
|
105
|
-
## How it works
|
|
106
|
-
|
|
107
|
-
```
|
|
108
|
-
await config()
|
|
109
|
-
└─ reads .suron.json # gets app_id and api_url
|
|
110
|
-
└─ POST /request-access # triggers Telegram approval message
|
|
111
|
-
└─ GET /status (polling) # waits for Approve / Deny tap
|
|
112
|
-
└─ POST /fetch-key # retrieves private key (single-use token)
|
|
113
|
-
└─ dotenvx.parse(.env, key) # decrypts secrets into process.env
|
|
114
|
-
```
|
|
115
|
-
|
|
116
|
-
The private key never touches disk during runtime. It's received in memory, used to decrypt, and the reference is dropped immediately after.
|
|
117
|
-
|
|
118
|
-
---
|
|
119
|
-
|
|
120
|
-
## Security
|
|
121
|
-
|
|
122
|
-
- The SDK reads only `.suron.json` for the `app_id` — no access to `~/.suron-config`
|
|
123
|
-
- The `MASTER_KEY` lives exclusively inside the Convex deployment — the SDK never sees it
|
|
124
|
-
- `/fetch-key` is single-use — the token is consumed on first retrieval
|
|
125
|
-
- `DOTENV_PUBLIC_KEY` and `DOTENV_PRIVATE_KEY` are filtered out of `process.env` after decryption
|
|
126
|
-
|
|
127
|
-
---
|
|
128
|
-
|
|
129
|
-
## Related
|
|
130
|
-
|
|
131
|
-
- [`@suronai/cli`](https://www.npmjs.com/package/@suronai/cli) — the CLI for setup and key rotation
|
package/index.d.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
export interface VaultOptions {
|
|
2
|
-
/** Directory containing .suron.json. Default: process.cwd() */
|
|
3
|
-
configPath?: string;
|
|
4
|
-
/** Milliseconds to wait for Telegram approval. Default: 300000 (5 min) */
|
|
5
|
-
timeout?: number;
|
|
6
|
-
/** Milliseconds between status polls. Default: 3000 */
|
|
7
|
-
pollInterval?: number;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Drop-in replacement for `dotenv`'s config().
|
|
12
|
-
*
|
|
13
|
-
* Waits for Telegram approval, then decrypts .env into process.env.
|
|
14
|
-
* Handles all known Suron errors with console.error + process.exit(1).
|
|
15
|
-
* Unknown errors are re-thrown.
|
|
16
|
-
*
|
|
17
|
-
* Usage:
|
|
18
|
-
* import { config } from '@suronai/sdk'
|
|
19
|
-
* await config()
|
|
20
|
-
*
|
|
21
|
-
* Must be awaited before any code that reads process.env.
|
|
22
|
-
*/
|
|
23
|
-
export function config(options?: VaultOptions): Promise<void>;
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
* Low-level vault. Waits for Telegram approval, then decrypts .env into process.env.
|
|
27
|
-
* Throws on all errors — use config() if you want automatic error handling.
|
|
28
|
-
* Must be awaited before any code that reads process.env.
|
|
29
|
-
*/
|
|
30
|
-
export function vault(options?: VaultOptions): Promise<void>;
|
|
31
|
-
|
|
32
|
-
export class SuronError extends Error {}
|
|
33
|
-
export class SuronConfigError extends SuronError {}
|
|
34
|
-
export class SuronAppNotFoundError extends SuronError {}
|
|
35
|
-
export class SuronRateLimitError extends SuronError {}
|
|
36
|
-
export class SuronDeniedError extends SuronError {}
|
|
37
|
-
export class SuronTimeoutError extends SuronError {}
|
package/src/poll.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { SuronDeniedError, SuronTimeoutError } from "./errors.js";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Polls /status until the request is approved, denied, or times out.
|
|
5
|
-
* @param {string} apiUrl
|
|
6
|
-
* @param {string} requestId
|
|
7
|
-
* @param {number} [timeout]
|
|
8
|
-
* @param {number} [pollInterval]
|
|
9
|
-
* @returns {Promise<void>}
|
|
10
|
-
*/
|
|
11
|
-
export async function pollUntilApproved(
|
|
12
|
-
apiUrl,
|
|
13
|
-
requestId,
|
|
14
|
-
timeout = 300_000,
|
|
15
|
-
pollInterval = 3_000
|
|
16
|
-
) {
|
|
17
|
-
const deadline = Date.now() + timeout;
|
|
18
|
-
|
|
19
|
-
while (Date.now() < deadline) {
|
|
20
|
-
await sleep(pollInterval);
|
|
21
|
-
|
|
22
|
-
let res;
|
|
23
|
-
try {
|
|
24
|
-
res = await fetch(`${apiUrl}/status?request_id=${encodeURIComponent(requestId)}`);
|
|
25
|
-
} catch {
|
|
26
|
-
// Network hiccup — keep polling
|
|
27
|
-
continue;
|
|
28
|
-
}
|
|
29
|
-
|
|
30
|
-
if (!res.ok) continue; // Transient error — keep polling
|
|
31
|
-
|
|
32
|
-
let status;
|
|
33
|
-
try {
|
|
34
|
-
({ status } = await res.json());
|
|
35
|
-
} catch {
|
|
36
|
-
// Malformed JSON — treat as transient and keep polling
|
|
37
|
-
continue;
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
if (status === "approved") return;
|
|
41
|
-
if (status === "denied") {
|
|
42
|
-
throw new SuronDeniedError("Boot denied — you tapped Deny in Telegram.");
|
|
43
|
-
}
|
|
44
|
-
// "pending" → keep polling
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
throw new SuronTimeoutError(
|
|
48
|
-
`No Telegram approval received within ${timeout / 1000}s.`
|
|
49
|
-
);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
/** @param {number} ms @returns {Promise<void>} */
|
|
53
|
-
function sleep(ms) {
|
|
54
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
55
|
-
}
|