@scrt-link/cli 0.0.1

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.
Files changed (2) hide show
  1. package/dist/index.js +217 -0
  2. package/package.json +46 -0
package/dist/index.js ADDED
@@ -0,0 +1,217 @@
1
+ #!/usr/bin/env node
2
+
3
+ // ../client/dist/index.js
4
+ var MASTER_PASSWORD_LENGTH = 36;
5
+ var SECRET_ID_LENGTH = 28;
6
+ var getRandomBytes = (length = 16) => crypto.getRandomValues(new Uint8Array(length));
7
+ var blobToBase64 = async (blob) => {
8
+ const buffer = await blob.arrayBuffer();
9
+ const bytes = new Uint8Array(buffer);
10
+ let binary = "";
11
+ for (const byte of bytes) {
12
+ binary += String.fromCharCode(byte);
13
+ }
14
+ return `data:application/octet-stream;base64,${btoa(binary)}`;
15
+ };
16
+ var encodeText = (text) => {
17
+ const encoder = new TextEncoder();
18
+ return encoder.encode(text);
19
+ };
20
+ var generateRandomUrlSafeString = (length = 36) => {
21
+ const letters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
22
+ const numbers = "0123456789";
23
+ const specialCharacters = "$~-_.";
24
+ const charset = [...letters, ...numbers, ...specialCharacters];
25
+ const values = getRandomBytes(length);
26
+ return Array.from(values, (v) => charset[v % charset.length]).join("");
27
+ };
28
+ var sha256Hash = async (message) => {
29
+ const buffer = await crypto.subtle.digest("SHA-256", encodeText(message));
30
+ return Array.prototype.map.call(new Uint8Array(buffer), (x) => ("00" + x.toString(16)).slice(-2)).join("");
31
+ };
32
+ var binaryToBase64 = (arrayBuffer) => {
33
+ const arrayBufferToString = (buffer) => String.fromCharCode(...new Uint8Array(buffer));
34
+ return btoa(arrayBufferToString(arrayBuffer));
35
+ };
36
+ var encryptData = async (data, cryptoKey, salt) => {
37
+ const iv = getRandomBytes();
38
+ const result = await crypto.subtle.encrypt({ name: "AES-GCM", iv }, cryptoKey, data);
39
+ const encryptedBlob = new Blob([salt, iv, result]);
40
+ return encryptedBlob;
41
+ };
42
+ var generateKeyFromPassword = async (password) => {
43
+ const salt = getRandomBytes();
44
+ const passwordKey = await crypto.subtle.importKey("raw", encodeText(password), "PBKDF2", false, [
45
+ "deriveKey"
46
+ ]);
47
+ const cryptoKey = await crypto.subtle.deriveKey(
48
+ {
49
+ name: "PBKDF2",
50
+ salt,
51
+ iterations: 1e5,
52
+ hash: "SHA-256"
53
+ },
54
+ passwordKey,
55
+ {
56
+ name: "AES-GCM",
57
+ length: 256
58
+ },
59
+ false,
60
+ ["encrypt"]
61
+ );
62
+ return {
63
+ cryptoKey,
64
+ salt
65
+ };
66
+ };
67
+ var encryptString = async (text, password) => {
68
+ const data = encodeText(text).buffer;
69
+ const { cryptoKey, salt } = await generateKeyFromPassword(password);
70
+ const encryptedData = await encryptData(data, cryptoKey, salt);
71
+ const encryptedDataBase64 = await blobToBase64(encryptedData);
72
+ return encryptedDataBase64;
73
+ };
74
+ var generateKeyPair = async () => await crypto.subtle.generateKey(
75
+ {
76
+ name: "ECDSA",
77
+ namedCurve: "P-384"
78
+ },
79
+ true,
80
+ ["sign", "verify"]
81
+ );
82
+ var pemHeader = "-----BEGIN PUBLIC KEY-----";
83
+ var pemFooter = "-----END PUBLIC KEY-----";
84
+ var exportPublicKey = async (key) => {
85
+ const exported = await crypto.subtle.exportKey("spki", key);
86
+ const exportedAsBase64 = binaryToBase64(exported);
87
+ const pemExported = `${pemHeader}
88
+ ${exportedAsBase64}
89
+ ${pemFooter}`;
90
+ return pemExported;
91
+ };
92
+ var SecretType = /* @__PURE__ */ ((SecretType2) => {
93
+ SecretType2["TEXT"] = "text";
94
+ SecretType2["FILE"] = "file";
95
+ SecretType2["REDIRECT"] = "redirect";
96
+ SecretType2["SNAP"] = "snap";
97
+ SecretType2["NEOGRAM"] = "neogram";
98
+ return SecretType2;
99
+ })(SecretType || {});
100
+ var MIN = 1e3 * 60;
101
+ var DAY = 24 * 60 * MIN;
102
+ var PROTOCOL = "https";
103
+ var DEFAULT_HOST = "scrt.link";
104
+ var scrtLink = (apiKey) => {
105
+ if (!apiKey) {
106
+ throw new Error("API key is required");
107
+ }
108
+ const createSecret = async (secret, options = {}) => {
109
+ const {
110
+ secretType = "text",
111
+ password,
112
+ publicNote,
113
+ expiresIn = 7 * DAY,
114
+ viewLimit = 1,
115
+ host = DEFAULT_HOST
116
+ } = options;
117
+ let encryptedContent = secret;
118
+ let encryptedMeta = JSON.stringify({
119
+ secretType
120
+ });
121
+ const masterKey = generateRandomUrlSafeString(MASTER_PASSWORD_LENGTH);
122
+ const keyPair = await generateKeyPair();
123
+ const secretIdSubstring = masterKey.substring(SECRET_ID_LENGTH);
124
+ const secretIdHash = await sha256Hash(secretIdSubstring);
125
+ const publicKey = await exportPublicKey(keyPair.publicKey);
126
+ if (password) {
127
+ encryptedMeta = await encryptString(encryptedMeta, password);
128
+ encryptedContent = await encryptString(encryptedContent, password);
129
+ }
130
+ encryptedMeta = await encryptString(encryptedMeta, masterKey);
131
+ encryptedContent = await encryptString(encryptedContent, masterKey);
132
+ const body = JSON.stringify({
133
+ secretIdHash,
134
+ meta: encryptedMeta,
135
+ content: encryptedContent,
136
+ publicKey,
137
+ publicNote,
138
+ expiresIn,
139
+ viewLimit,
140
+ password
141
+ });
142
+ const checksum = await sha256Hash(body);
143
+ const res = await fetch(`${PROTOCOL}://${host}/api/v1/secrets`, {
144
+ method: "POST",
145
+ headers: {
146
+ "Content-Type": "application/json",
147
+ Authorization: `Bearer ${apiKey}`,
148
+ "X-Checksum": checksum,
149
+ ...host ? { "X-Host": host } : {}
150
+ },
151
+ body
152
+ });
153
+ const data = await res.json();
154
+ if (!res.ok) {
155
+ return data;
156
+ }
157
+ const secretLink = `${PROTOCOL}://${host}/s#${masterKey}`;
158
+ return { secretLink, ...data };
159
+ };
160
+ return {
161
+ createSecret
162
+ };
163
+ };
164
+ var index_default = scrtLink;
165
+
166
+ // src/index.ts
167
+ import { program } from "commander";
168
+ var EXPIRES_MAP = {
169
+ "1h": 60 * 60 * 1e3,
170
+ "1d": 24 * 60 * 60 * 1e3,
171
+ "1w": 7 * 24 * 60 * 60 * 1e3,
172
+ "1m": 30 * 24 * 60 * 60 * 1e3
173
+ };
174
+ var TYPE_MAP = {
175
+ text: SecretType.TEXT,
176
+ redirect: SecretType.REDIRECT,
177
+ neogram: SecretType.NEOGRAM
178
+ };
179
+ program.name("scrtlink").description("Create end-to-end encrypted secrets from the command line").version("0.0.1").argument("<secret>", "The secret content to encrypt and share").option("--type <type>", "Secret type: text | redirect | neogram (default: text)").option("--expires <duration>", "Expiration: 1h | 1d | 1w | 1m (default: 1w)").option("--views <n>", "View limit 1\u20131000 (default: 1)").option("--note <text>", "Public note visible to the recipient before revealing").option("--password <pass>", "Password-protect the secret").option("--host <host>", "API host for self-hosted instances (default: scrt.link)").option("--api-key <key>", "API key (or set SCRT_LINK_API_KEY env var)").action(async (secret, opts) => {
180
+ const apiKey = opts.apiKey ?? process.env.SCRT_LINK_API_KEY ?? "";
181
+ if (!apiKey) {
182
+ console.error("Error: API key required. Use --api-key or set SCRT_LINK_API_KEY.");
183
+ process.exit(1);
184
+ }
185
+ const expiresKey = opts.expires ?? "1w";
186
+ if (!(expiresKey in EXPIRES_MAP)) {
187
+ console.error(
188
+ `Error: Invalid --expires value "${expiresKey}". Allowed: ${Object.keys(EXPIRES_MAP).join(", ")}.`
189
+ );
190
+ process.exit(1);
191
+ }
192
+ const expiresIn = EXPIRES_MAP[expiresKey];
193
+ const viewLimit = opts.views !== void 0 ? parseInt(opts.views, 10) : 1;
194
+ const secretType = TYPE_MAP[opts.type ?? "text"] ?? SecretType.TEXT;
195
+ try {
196
+ const client = index_default(apiKey);
197
+ const result = await client.createSecret(secret, {
198
+ secretType,
199
+ expiresIn,
200
+ viewLimit,
201
+ publicNote: opts.note,
202
+ password: opts.password,
203
+ host: opts.host
204
+ });
205
+ if ("secretLink" in result && result.secretLink) {
206
+ process.stdout.write(result.secretLink + "\n");
207
+ } else {
208
+ const errResult = result;
209
+ console.error("Error:", errResult.error ?? errResult.message ?? JSON.stringify(result));
210
+ process.exit(1);
211
+ }
212
+ } catch (err) {
213
+ console.error("Error:", err instanceof Error ? err.message : String(err));
214
+ process.exit(1);
215
+ }
216
+ });
217
+ program.parse();
package/package.json ADDED
@@ -0,0 +1,46 @@
1
+ {
2
+ "name": "@scrt-link/cli",
3
+ "version": "0.0.1",
4
+ "description": "CLI for scrt.link — create encrypted secrets from the command line",
5
+ "keywords": [
6
+ "scrt.link",
7
+ "secrets",
8
+ "cli",
9
+ "encryption",
10
+ "security"
11
+ ],
12
+ "homepage": "https://github.com/stophecom/scrt-link-v2#readme",
13
+ "bugs": {
14
+ "url": "https://github.com/stophecom/scrt-link-v2/issues"
15
+ },
16
+ "repository": {
17
+ "type": "git",
18
+ "url": "git+https://github.com/stophecom/scrt-link-v2.git"
19
+ },
20
+ "license": "MIT",
21
+ "author": "stophe",
22
+ "type": "module",
23
+ "bin": {
24
+ "scrtlink": "./dist/index.js"
25
+ },
26
+ "files": [
27
+ "dist"
28
+ ],
29
+ "scripts": {
30
+ "build": "tsup",
31
+ "dev": "tsup --watch",
32
+ "prepublishOnly": "npm run build"
33
+ },
34
+ "dependencies": {
35
+ "@scrt-link/client": "workspace:*",
36
+ "commander": "^12.0.0"
37
+ },
38
+ "devDependencies": {
39
+ "@types/node": "^25.5.2",
40
+ "tsup": "^8.0.0",
41
+ "typescript": "^5.0.0"
42
+ },
43
+ "engines": {
44
+ "node": ">=18"
45
+ }
46
+ }