@thisisamank/noenv-cli 0.0.0-stage → 0.1.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.
- package/README.md +57 -2
- package/dist/noenv.js +2062 -0
- package/package.json +41 -4
package/dist/noenv.js
ADDED
|
@@ -0,0 +1,2062 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// package.json
|
|
4
|
+
var package_default = {
|
|
5
|
+
name: "@thisisamank/noenv-cli",
|
|
6
|
+
version: "0.1.1",
|
|
7
|
+
description: "Encrypted environment variables that load when you cd into a repo. CLI for noenv.",
|
|
8
|
+
type: "module",
|
|
9
|
+
bin: {
|
|
10
|
+
noenv: "dist/noenv.js"
|
|
11
|
+
},
|
|
12
|
+
files: [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
engines: {
|
|
16
|
+
node: ">=22"
|
|
17
|
+
},
|
|
18
|
+
repository: {
|
|
19
|
+
type: "git",
|
|
20
|
+
url: "git+https://github.com/thisisamank/noenv.git",
|
|
21
|
+
directory: "cli"
|
|
22
|
+
},
|
|
23
|
+
homepage: "https://github.com/thisisamank/noenv#readme",
|
|
24
|
+
keywords: [
|
|
25
|
+
"secrets",
|
|
26
|
+
"env",
|
|
27
|
+
"dotenv",
|
|
28
|
+
"cloudflare",
|
|
29
|
+
"cli"
|
|
30
|
+
],
|
|
31
|
+
scripts: {
|
|
32
|
+
build: "esbuild src/main.ts --bundle --platform=node --format=esm --target=node22 --outfile=dist/noenv.js",
|
|
33
|
+
prepublishOnly: "node ../scripts/check-npm-account.ts && npm run build"
|
|
34
|
+
},
|
|
35
|
+
devDependencies: {
|
|
36
|
+
"@noenv/config": "*",
|
|
37
|
+
"@noenv/crypto": "*",
|
|
38
|
+
"@noenv/dotenv": "*",
|
|
39
|
+
"@noenv/policy": "*",
|
|
40
|
+
"@noenv/worker": "*",
|
|
41
|
+
esbuild: "^0.28.2"
|
|
42
|
+
},
|
|
43
|
+
publishConfig: {
|
|
44
|
+
access: "public"
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
// src/version.ts
|
|
49
|
+
var CLI_VERSION = package_default.version;
|
|
50
|
+
|
|
51
|
+
// src/api.ts
|
|
52
|
+
var TIMEOUT_MS = 1e4;
|
|
53
|
+
var ApiError = class extends Error {
|
|
54
|
+
status;
|
|
55
|
+
constructor(status, message) {
|
|
56
|
+
super(message);
|
|
57
|
+
this.status = status;
|
|
58
|
+
}
|
|
59
|
+
};
|
|
60
|
+
async function api(deployment, method, path, options = {}) {
|
|
61
|
+
const headers = { "X-Noenv-Version": CLI_VERSION };
|
|
62
|
+
if (options.token) headers.Authorization = `Bearer ${options.token}`;
|
|
63
|
+
if (options.body !== void 0) headers["Content-Type"] = "application/json";
|
|
64
|
+
const response = await deployment.fetch(deployment.url + path, {
|
|
65
|
+
method,
|
|
66
|
+
headers,
|
|
67
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
68
|
+
redirect: "manual",
|
|
69
|
+
signal: AbortSignal.timeout(TIMEOUT_MS)
|
|
70
|
+
});
|
|
71
|
+
if (response.status >= 300 && response.status < 400) {
|
|
72
|
+
throw new ApiError(response.status, `${deployment.url} redirected the request, check that Cloudflare Access does not protect /v1`);
|
|
73
|
+
}
|
|
74
|
+
const data = response.status === 204 ? null : await response.json().catch(() => void 0);
|
|
75
|
+
if (data === void 0) {
|
|
76
|
+
throw new ApiError(response.status, `${deployment.url} did not return a noenv API response (status ${response.status})`);
|
|
77
|
+
}
|
|
78
|
+
if (!response.ok) {
|
|
79
|
+
throw new ApiError(response.status, data?.error ?? `request failed with status ${response.status}`);
|
|
80
|
+
}
|
|
81
|
+
return data;
|
|
82
|
+
}
|
|
83
|
+
function repoPath(repo) {
|
|
84
|
+
return `/v1/repos/${encodeURIComponent(repo)}`;
|
|
85
|
+
}
|
|
86
|
+
function environmentPath(repo, environment) {
|
|
87
|
+
return `${repoPath(repo)}/environments/${environment}`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/deployment.ts
|
|
91
|
+
import { homedir } from "node:os";
|
|
92
|
+
import { join } from "node:path";
|
|
93
|
+
function currentDeployment(env = process.env) {
|
|
94
|
+
if (!env.NOENV_URL) return null;
|
|
95
|
+
const url = URL.parse(env.NOENV_URL);
|
|
96
|
+
if (!url || url.protocol !== "https:" && url.protocol !== "http:") {
|
|
97
|
+
throw new Error("NOENV_URL must be an http or https URL");
|
|
98
|
+
}
|
|
99
|
+
const base = env.NOENV_CONFIG_DIR ?? join(env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), "noenv");
|
|
100
|
+
return { url: url.origin, dir: join(base, url.host.replace(":", "_")), fetch: globalThis.fetch };
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// src/identity.ts
|
|
104
|
+
import { join as join2 } from "node:path";
|
|
105
|
+
|
|
106
|
+
// ../packages/crypto/src/encoding.ts
|
|
107
|
+
var encoder = new TextEncoder();
|
|
108
|
+
var decoder = new TextDecoder();
|
|
109
|
+
function toBytes(text3) {
|
|
110
|
+
return encoder.encode(text3);
|
|
111
|
+
}
|
|
112
|
+
function fromBytes(bytes) {
|
|
113
|
+
return decoder.decode(bytes);
|
|
114
|
+
}
|
|
115
|
+
function toBase64(bytes) {
|
|
116
|
+
let binary = "";
|
|
117
|
+
for (const byte of bytes) binary += String.fromCharCode(byte);
|
|
118
|
+
return btoa(binary);
|
|
119
|
+
}
|
|
120
|
+
function fromBase64(text3) {
|
|
121
|
+
return Uint8Array.from(atob(text3), (char) => char.charCodeAt(0));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// ../packages/crypto/src/keys.ts
|
|
125
|
+
var ECDH = { name: "ECDH", namedCurve: "P-256" };
|
|
126
|
+
async function generateKeyPair() {
|
|
127
|
+
return crypto.subtle.generateKey(ECDH, true, ["deriveBits"]);
|
|
128
|
+
}
|
|
129
|
+
async function exportPublicKey(key) {
|
|
130
|
+
return toBase64(new Uint8Array(await crypto.subtle.exportKey("raw", key)));
|
|
131
|
+
}
|
|
132
|
+
async function importPublicKey(text3) {
|
|
133
|
+
return crypto.subtle.importKey("raw", fromBase64(text3), ECDH, true, []);
|
|
134
|
+
}
|
|
135
|
+
async function exportPrivateKey(key) {
|
|
136
|
+
return toBase64(new Uint8Array(await crypto.subtle.exportKey("pkcs8", key)));
|
|
137
|
+
}
|
|
138
|
+
async function importPrivateKey(text3) {
|
|
139
|
+
return crypto.subtle.importKey("pkcs8", fromBase64(text3), ECDH, true, ["deriveBits"]);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ../packages/crypto/src/aead.ts
|
|
143
|
+
var AES_GCM = { name: "AES-GCM", length: 256 };
|
|
144
|
+
var IV_BYTES = 12;
|
|
145
|
+
async function seal(key, plaintext, context) {
|
|
146
|
+
const iv = crypto.getRandomValues(new Uint8Array(IV_BYTES));
|
|
147
|
+
const ciphertext = await crypto.subtle.encrypt(
|
|
148
|
+
{ name: "AES-GCM", iv, additionalData: context },
|
|
149
|
+
key,
|
|
150
|
+
plaintext
|
|
151
|
+
);
|
|
152
|
+
return { iv: toBase64(iv), ciphertext: toBase64(new Uint8Array(ciphertext)) };
|
|
153
|
+
}
|
|
154
|
+
async function open(key, sealed, context) {
|
|
155
|
+
const plaintext = await crypto.subtle.decrypt(
|
|
156
|
+
{ name: "AES-GCM", iv: fromBase64(sealed.iv), additionalData: context },
|
|
157
|
+
key,
|
|
158
|
+
fromBase64(sealed.ciphertext)
|
|
159
|
+
);
|
|
160
|
+
return new Uint8Array(plaintext);
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ../packages/crypto/src/box.ts
|
|
164
|
+
async function sealTo(recipientPublicKey, plaintext, context) {
|
|
165
|
+
const ephemeral = await generateKeyPair();
|
|
166
|
+
const ephemeralPublicKey = await exportPublicKey(ephemeral.publicKey);
|
|
167
|
+
const key = await deriveKey(ephemeral.privateKey, recipientPublicKey, ephemeralPublicKey, context);
|
|
168
|
+
return { ...await seal(key, plaintext, context), ephemeralPublicKey };
|
|
169
|
+
}
|
|
170
|
+
async function openWith(recipientPrivateKey, sealed, context) {
|
|
171
|
+
const ephemeralKey = await importPublicKey(sealed.ephemeralPublicKey);
|
|
172
|
+
const key = await deriveKey(recipientPrivateKey, ephemeralKey, sealed.ephemeralPublicKey, context);
|
|
173
|
+
return open(key, sealed, context);
|
|
174
|
+
}
|
|
175
|
+
async function deriveKey(privateKey, publicKey, ephemeralPublicKey, context) {
|
|
176
|
+
const shared = await crypto.subtle.deriveBits({ name: "ECDH", public: publicKey }, privateKey, 256);
|
|
177
|
+
const hkdfKey = await crypto.subtle.importKey("raw", shared, "HKDF", false, ["deriveKey"]);
|
|
178
|
+
return crypto.subtle.deriveKey(
|
|
179
|
+
{ name: "HKDF", hash: "SHA-256", salt: fromBase64(ephemeralPublicKey), info: context },
|
|
180
|
+
hkdfKey,
|
|
181
|
+
AES_GCM,
|
|
182
|
+
false,
|
|
183
|
+
["encrypt", "decrypt"]
|
|
184
|
+
);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ../packages/crypto/src/secrets.ts
|
|
188
|
+
async function encryptSecret(environmentPublicKey, value, context) {
|
|
189
|
+
return sealTo(environmentPublicKey, toBytes(value), contextBytes(context));
|
|
190
|
+
}
|
|
191
|
+
async function decryptSecret(environmentPrivateKey, sealed, context) {
|
|
192
|
+
return fromBytes(await openWith(environmentPrivateKey, sealed, contextBytes(context)));
|
|
193
|
+
}
|
|
194
|
+
function contextBytes({ projectId, environment, name: name2 }) {
|
|
195
|
+
return toBytes(`noenv-secret-v1\0${projectId}\0${environment}\0${name2}`);
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
// ../packages/crypto/src/wrap.ts
|
|
199
|
+
var WRAP_CONTEXT = toBytes("noenv-key-wrap-v1");
|
|
200
|
+
async function wrapPrivateKey(privateKey, recipientPublicKey) {
|
|
201
|
+
const pkcs8 = new Uint8Array(await crypto.subtle.exportKey("pkcs8", privateKey));
|
|
202
|
+
return sealTo(recipientPublicKey, pkcs8, WRAP_CONTEXT);
|
|
203
|
+
}
|
|
204
|
+
async function unwrapPrivateKey(wrapped, recipientPrivateKey) {
|
|
205
|
+
const pkcs8 = await openWith(recipientPrivateKey, wrapped, WRAP_CONTEXT);
|
|
206
|
+
return crypto.subtle.importKey("pkcs8", pkcs8, ECDH, true, ["deriveBits"]);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// src/files.ts
|
|
210
|
+
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
211
|
+
import { dirname } from "node:path";
|
|
212
|
+
async function readJsonFile(path) {
|
|
213
|
+
try {
|
|
214
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
215
|
+
} catch (error) {
|
|
216
|
+
if (error.code === "ENOENT") return null;
|
|
217
|
+
throw error;
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
async function writeJsonFile(path, value, flag = "w") {
|
|
221
|
+
await mkdir(dirname(path), { recursive: true, mode: 448 });
|
|
222
|
+
await writeFile(path, JSON.stringify(value), { mode: 384, flag });
|
|
223
|
+
}
|
|
224
|
+
async function removeFile(path) {
|
|
225
|
+
await rm(path, { force: true });
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
// src/identity.ts
|
|
229
|
+
async function loadIdentity(deployment) {
|
|
230
|
+
const stored = await readJsonFile(identityPath(deployment));
|
|
231
|
+
if (!stored) return null;
|
|
232
|
+
return { publicKey: stored.publicKey, privateKey: await importPrivateKey(stored.privateKey) };
|
|
233
|
+
}
|
|
234
|
+
async function ensureIdentity(deployment, token) {
|
|
235
|
+
const local = await loadIdentity(deployment);
|
|
236
|
+
const me = await api(deployment, "GET", "/v1/me", { token });
|
|
237
|
+
if (local && me.publicKey === local.publicKey) return local;
|
|
238
|
+
if (me.publicKey) {
|
|
239
|
+
throw new Error(`your noenv key was registered on another device, copy ${identityPath(deployment)} from that device`);
|
|
240
|
+
}
|
|
241
|
+
const identity = local ?? await createIdentity(deployment);
|
|
242
|
+
await api(deployment, "PUT", "/v1/me/public-key", { token, body: { publicKey: identity.publicKey } });
|
|
243
|
+
return identity;
|
|
244
|
+
}
|
|
245
|
+
async function createIdentity(deployment) {
|
|
246
|
+
const pair = await generateKeyPair();
|
|
247
|
+
const stored = {
|
|
248
|
+
publicKey: await exportPublicKey(pair.publicKey),
|
|
249
|
+
privateKey: await exportPrivateKey(pair.privateKey)
|
|
250
|
+
};
|
|
251
|
+
await writeJsonFile(identityPath(deployment), stored, "wx");
|
|
252
|
+
return { publicKey: stored.publicKey, privateKey: pair.privateKey };
|
|
253
|
+
}
|
|
254
|
+
function identityPath(deployment) {
|
|
255
|
+
return join2(deployment.dir, "identity.json");
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
// src/session.ts
|
|
259
|
+
import { spawn } from "node:child_process";
|
|
260
|
+
import { hostname } from "node:os";
|
|
261
|
+
import { join as join3 } from "node:path";
|
|
262
|
+
async function loadSession(deployment) {
|
|
263
|
+
const session = await readJsonFile(sessionPath(deployment));
|
|
264
|
+
if (!session) return null;
|
|
265
|
+
return Date.parse(`${session.expiresAt.replace(" ", "T")}Z`) > Date.now() ? session.token : null;
|
|
266
|
+
}
|
|
267
|
+
async function clearSession(deployment) {
|
|
268
|
+
await removeFile(sessionPath(deployment));
|
|
269
|
+
}
|
|
270
|
+
async function requireSession(deployment) {
|
|
271
|
+
const token = await loadSession(deployment);
|
|
272
|
+
if (!token) {
|
|
273
|
+
throw new Error("you are not signed in, open a new shell or run `noenv auth`");
|
|
274
|
+
}
|
|
275
|
+
return token;
|
|
276
|
+
}
|
|
277
|
+
async function startSignIn(deployment) {
|
|
278
|
+
const existing = await readJsonFile(pendingPath(deployment));
|
|
279
|
+
if (existing && existing.expiresAt > Date.now()) {
|
|
280
|
+
return { pending: existing, started: false };
|
|
281
|
+
}
|
|
282
|
+
await removeFile(pendingPath(deployment));
|
|
283
|
+
const device = await api(deployment, "POST", "/v1/auth/device");
|
|
284
|
+
if (!device.verificationUriComplete.startsWith(`${deployment.url}/activate`)) {
|
|
285
|
+
throw new Error("the server returned an unexpected sign-in URL");
|
|
286
|
+
}
|
|
287
|
+
const pending = {
|
|
288
|
+
deviceCode: device.deviceCode,
|
|
289
|
+
userCode: device.userCode,
|
|
290
|
+
url: device.verificationUriComplete,
|
|
291
|
+
interval: device.interval,
|
|
292
|
+
expiresAt: Date.now() + device.expiresIn * 1e3
|
|
293
|
+
};
|
|
294
|
+
try {
|
|
295
|
+
await writeJsonFile(pendingPath(deployment), pending, "wx");
|
|
296
|
+
} catch (error) {
|
|
297
|
+
if (error.code !== "EEXIST") throw error;
|
|
298
|
+
return startSignIn(deployment);
|
|
299
|
+
}
|
|
300
|
+
return { pending, started: true };
|
|
301
|
+
}
|
|
302
|
+
async function waitForSignIn(deployment, pending) {
|
|
303
|
+
while (pending.expiresAt > Date.now()) {
|
|
304
|
+
await sleep(pending.interval * 1e3);
|
|
305
|
+
try {
|
|
306
|
+
const session = await api(deployment, "POST", "/v1/auth/token", {
|
|
307
|
+
body: { deviceCode: pending.deviceCode, deviceName: hostname().slice(0, 100) }
|
|
308
|
+
});
|
|
309
|
+
await writeJsonFile(sessionPath(deployment), session);
|
|
310
|
+
await removeFile(pendingPath(deployment));
|
|
311
|
+
await ensureIdentity(deployment, session.token);
|
|
312
|
+
return true;
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if (!(error instanceof ApiError) || error.message !== "authorization_pending") break;
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
await removeFile(pendingPath(deployment));
|
|
318
|
+
return await loadSession(deployment) !== null;
|
|
319
|
+
}
|
|
320
|
+
async function readPendingSignIn(deployment) {
|
|
321
|
+
return readJsonFile(pendingPath(deployment));
|
|
322
|
+
}
|
|
323
|
+
function openBrowser(url) {
|
|
324
|
+
const command2 = process.platform === "darwin" ? "open" : "xdg-open";
|
|
325
|
+
spawn(command2, [url], { detached: true, stdio: "ignore" }).on("error", () => {
|
|
326
|
+
}).unref();
|
|
327
|
+
}
|
|
328
|
+
function startBackgroundPoller() {
|
|
329
|
+
spawn(process.execPath, [...process.execArgv, process.argv[1], "auth", "--wait"], {
|
|
330
|
+
detached: true,
|
|
331
|
+
stdio: "ignore"
|
|
332
|
+
}).unref();
|
|
333
|
+
}
|
|
334
|
+
function sessionPath(deployment) {
|
|
335
|
+
return join3(deployment.dir, "session.json");
|
|
336
|
+
}
|
|
337
|
+
function pendingPath(deployment) {
|
|
338
|
+
return join3(deployment.dir, "pending-sign-in.json");
|
|
339
|
+
}
|
|
340
|
+
function sleep(ms) {
|
|
341
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
// ../packages/config/src/errors.ts
|
|
345
|
+
var ConfigError = class extends Error {
|
|
346
|
+
};
|
|
347
|
+
|
|
348
|
+
// ../node_modules/smol-toml/dist/error.js
|
|
349
|
+
function getLineColFromPtr(string, ptr) {
|
|
350
|
+
let lines = string.slice(0, ptr).split(/\r?\n/);
|
|
351
|
+
return [lines.length, lines.pop().length + 1];
|
|
352
|
+
}
|
|
353
|
+
function makeCodeBlock(string, line, column) {
|
|
354
|
+
let lines = string.split(/\r?\n/);
|
|
355
|
+
let codeblock = "";
|
|
356
|
+
let numberLen = (Math.log10(line + 1) | 0) + 1;
|
|
357
|
+
for (let i = line - 1; i <= line + 1; i++) {
|
|
358
|
+
let l = lines[i - 1];
|
|
359
|
+
if (!l)
|
|
360
|
+
continue;
|
|
361
|
+
codeblock += i.toString().padEnd(numberLen, " ");
|
|
362
|
+
codeblock += ": ";
|
|
363
|
+
codeblock += l;
|
|
364
|
+
codeblock += "\n";
|
|
365
|
+
if (i === line) {
|
|
366
|
+
codeblock += " ".repeat(numberLen + column + 2);
|
|
367
|
+
codeblock += "^\n";
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
return codeblock;
|
|
371
|
+
}
|
|
372
|
+
var TomlError = class _TomlError extends Error {
|
|
373
|
+
line;
|
|
374
|
+
column;
|
|
375
|
+
codeblock;
|
|
376
|
+
constructor(message, options) {
|
|
377
|
+
const [line, column] = getLineColFromPtr(options.toml, options.ptr);
|
|
378
|
+
const codeblock = makeCodeBlock(options.toml, line, column);
|
|
379
|
+
super(`Invalid TOML document: ${message}
|
|
380
|
+
|
|
381
|
+
${codeblock}`, options);
|
|
382
|
+
this.line = line;
|
|
383
|
+
this.column = column;
|
|
384
|
+
this.codeblock = codeblock;
|
|
385
|
+
}
|
|
386
|
+
/** @internal */
|
|
387
|
+
static x(message, ctx, ptr) {
|
|
388
|
+
throw new _TomlError(message, { toml: ctx.s, ptr: ptr ?? ctx.p });
|
|
389
|
+
}
|
|
390
|
+
};
|
|
391
|
+
|
|
392
|
+
// ../node_modules/smol-toml/dist/primitive.js
|
|
393
|
+
function parseString(ctx) {
|
|
394
|
+
let startPtr = ctx.p;
|
|
395
|
+
let c = ctx.s.charCodeAt(ctx.p++);
|
|
396
|
+
let first = c;
|
|
397
|
+
let isLiteral = c === 39;
|
|
398
|
+
let isMultiline = c === ctx.s.charCodeAt(ctx.p) && c === ctx.s.charCodeAt(ctx.p + 1);
|
|
399
|
+
if (isMultiline) {
|
|
400
|
+
if ((c = ctx.s.charCodeAt(ctx.p += 2)) === 10)
|
|
401
|
+
ctx.p++;
|
|
402
|
+
else if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)
|
|
403
|
+
ctx.p += 2;
|
|
404
|
+
}
|
|
405
|
+
let parsed = "";
|
|
406
|
+
let sliceStart = ctx.p;
|
|
407
|
+
let state = 0;
|
|
408
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
409
|
+
c = ctx.s.charCodeAt(ctx.p);
|
|
410
|
+
if (isMultiline && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)) {
|
|
411
|
+
state = state && 3;
|
|
412
|
+
} else if (c < 32 && c !== 9 || c === 127) {
|
|
413
|
+
TomlError.x("control characters are not allowed in strings", ctx);
|
|
414
|
+
} else if ((!state || state === 3) && c === first && (!isMultiline || ctx.s.charCodeAt(ctx.p + 1) === first && ctx.s.charCodeAt(ctx.p + 2) === first)) {
|
|
415
|
+
if (isMultiline) {
|
|
416
|
+
if (ctx.s.charCodeAt(ctx.p + 3) === first)
|
|
417
|
+
ctx.p++;
|
|
418
|
+
if (ctx.s.charCodeAt(ctx.p + 3) === first)
|
|
419
|
+
ctx.p++;
|
|
420
|
+
}
|
|
421
|
+
if (!state) {
|
|
422
|
+
let s = ctx.s.slice(sliceStart, ctx.p);
|
|
423
|
+
parsed = parsed ? parsed + s : s;
|
|
424
|
+
}
|
|
425
|
+
ctx.p += isMultiline ? 3 : 1;
|
|
426
|
+
return parsed;
|
|
427
|
+
} else if (!state) {
|
|
428
|
+
if (!isLiteral && c === 92) {
|
|
429
|
+
parsed += ctx.s.slice(sliceStart, sliceStart = ctx.p);
|
|
430
|
+
state = 1;
|
|
431
|
+
}
|
|
432
|
+
} else if (state === 1) {
|
|
433
|
+
if (c === 120 || c === 117 || c === 85) {
|
|
434
|
+
let errPtr = ctx.p++ - 1;
|
|
435
|
+
let value = 0;
|
|
436
|
+
let len = c === 120 ? 2 : c === 117 ? 4 : 8;
|
|
437
|
+
for (let j = 0; j < len; j++, ctx.p++) {
|
|
438
|
+
let hex = ctx.s.charCodeAt(ctx.p);
|
|
439
|
+
let digit = (
|
|
440
|
+
/* 0-9 */
|
|
441
|
+
hex >= 48 && hex <= 57 ? hex - 48 : (
|
|
442
|
+
/* A-F */
|
|
443
|
+
hex >= 65 && hex <= 70 ? hex - 65 + 10 : (
|
|
444
|
+
/* a-f */
|
|
445
|
+
hex >= 97 && hex <= 102 ? hex - 97 + 10 : -1
|
|
446
|
+
)
|
|
447
|
+
)
|
|
448
|
+
);
|
|
449
|
+
if (digit < 0)
|
|
450
|
+
TomlError.x("invalid non-hex character in unicode escape", ctx);
|
|
451
|
+
value = value << 4 | digit;
|
|
452
|
+
}
|
|
453
|
+
if (value < 0 || value > 1114111 || value >= 55296 && value <= 57343) {
|
|
454
|
+
TomlError.x("invalid unicode escape", ctx, errPtr);
|
|
455
|
+
}
|
|
456
|
+
parsed += String.fromCodePoint(value);
|
|
457
|
+
sliceStart = ctx.p--;
|
|
458
|
+
state = 0;
|
|
459
|
+
} else if (isMultiline && (c === 32 || c === 9)) {
|
|
460
|
+
state = 2;
|
|
461
|
+
} else {
|
|
462
|
+
if (c === 98)
|
|
463
|
+
parsed += "\b";
|
|
464
|
+
else if (c === 116)
|
|
465
|
+
parsed += " ";
|
|
466
|
+
else if (c === 110)
|
|
467
|
+
parsed += "\n";
|
|
468
|
+
else if (c === 102)
|
|
469
|
+
parsed += "\f";
|
|
470
|
+
else if (c === 114)
|
|
471
|
+
parsed += "\r";
|
|
472
|
+
else if (c === 101)
|
|
473
|
+
parsed += "\x1B";
|
|
474
|
+
else if (c === 34)
|
|
475
|
+
parsed += '"';
|
|
476
|
+
else if (c === 92)
|
|
477
|
+
parsed += "\\";
|
|
478
|
+
else
|
|
479
|
+
TomlError.x("unrecognised escape sequence", ctx);
|
|
480
|
+
sliceStart = ctx.p + 1;
|
|
481
|
+
state = 0;
|
|
482
|
+
}
|
|
483
|
+
} else if (c !== 32 && c !== 9) {
|
|
484
|
+
if (state === 2)
|
|
485
|
+
TomlError.x("invalid escape: only line-ending whitespace may be escaped", ctx, sliceStart);
|
|
486
|
+
state = !isLiteral && c === 92 ? 1 : 0;
|
|
487
|
+
sliceStart = ctx.p;
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
TomlError.x("unfinished string", ctx, startPtr);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// ../node_modules/smol-toml/dist/date.js
|
|
494
|
+
var DATE_TIME_RE = /^(\d{4}-\d{2}-\d{2})?[Tt ]?(?:(\d{2}):\d{2}(?::\d{2}(?:\.\d+)?)?)?(Z|z|[-+]\d{2}:\d{2})?$/i;
|
|
495
|
+
var TomlDate = class _TomlDate extends Date {
|
|
496
|
+
#hasDate = false;
|
|
497
|
+
#hasTime = false;
|
|
498
|
+
#offset = null;
|
|
499
|
+
constructor(date, fasttype, unsafeDelim) {
|
|
500
|
+
let hasDate = true;
|
|
501
|
+
let hasTime = true;
|
|
502
|
+
let offset = "Z";
|
|
503
|
+
let c;
|
|
504
|
+
if (typeof date === "string") {
|
|
505
|
+
if (fasttype)
|
|
506
|
+
prep: {
|
|
507
|
+
if (fasttype < 3) {
|
|
508
|
+
if (+date.slice(11, 13) > 23) {
|
|
509
|
+
date = "";
|
|
510
|
+
break prep;
|
|
511
|
+
}
|
|
512
|
+
if (fasttype === 2) {
|
|
513
|
+
offset = null;
|
|
514
|
+
date += "Z";
|
|
515
|
+
} else if ((c = date.charCodeAt(date.length - 1)) !== 90 && c !== 122) {
|
|
516
|
+
offset = date.slice(date.length - 6);
|
|
517
|
+
}
|
|
518
|
+
if (unsafeDelim)
|
|
519
|
+
date = date.slice(0, 10) + "T" + date.slice(11);
|
|
520
|
+
} else if (fasttype === 4) {
|
|
521
|
+
date = +date.slice(0, 2) > 23 ? "" : `0000-01-01T${date}Z`;
|
|
522
|
+
}
|
|
523
|
+
hasDate = fasttype !== 4;
|
|
524
|
+
hasTime = fasttype !== 3;
|
|
525
|
+
}
|
|
526
|
+
else {
|
|
527
|
+
let match = date.match(DATE_TIME_RE);
|
|
528
|
+
if (match) {
|
|
529
|
+
if (!match[1]) {
|
|
530
|
+
hasDate = false;
|
|
531
|
+
date = `0000-01-01T${date}`;
|
|
532
|
+
}
|
|
533
|
+
hasTime = !!match[2];
|
|
534
|
+
hasTime && date[10] === " " && (date = date.replace(" ", "T"));
|
|
535
|
+
if (match[2] && +match[2] > 23) {
|
|
536
|
+
date = "";
|
|
537
|
+
} else {
|
|
538
|
+
offset = match[3] || null;
|
|
539
|
+
if (!offset && hasTime)
|
|
540
|
+
date += "Z";
|
|
541
|
+
}
|
|
542
|
+
} else {
|
|
543
|
+
date = "";
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
super(date);
|
|
548
|
+
if (!isNaN(this.getTime())) {
|
|
549
|
+
this.#hasDate = hasDate;
|
|
550
|
+
this.#hasTime = hasTime;
|
|
551
|
+
this.#offset = offset;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
isDateTime() {
|
|
555
|
+
return this.#hasDate && this.#hasTime;
|
|
556
|
+
}
|
|
557
|
+
isLocal() {
|
|
558
|
+
return !this.#hasDate || !this.#hasTime || !this.#offset;
|
|
559
|
+
}
|
|
560
|
+
isDate() {
|
|
561
|
+
return this.#hasDate && !this.#hasTime;
|
|
562
|
+
}
|
|
563
|
+
isTime() {
|
|
564
|
+
return this.#hasTime && !this.#hasDate;
|
|
565
|
+
}
|
|
566
|
+
isValid() {
|
|
567
|
+
return this.#hasDate || this.#hasTime;
|
|
568
|
+
}
|
|
569
|
+
toISOString() {
|
|
570
|
+
let iso = super.toISOString();
|
|
571
|
+
if (this.isDate())
|
|
572
|
+
return iso.slice(0, 10);
|
|
573
|
+
if (this.isTime())
|
|
574
|
+
return iso.slice(11, 23);
|
|
575
|
+
if (this.#offset === null)
|
|
576
|
+
return iso.slice(0, -1);
|
|
577
|
+
if (this.#offset === "Z" || this.#offset === "z")
|
|
578
|
+
return iso;
|
|
579
|
+
let offset = +this.#offset.slice(1, 3) * 60 + +this.#offset.slice(4, 6);
|
|
580
|
+
offset = this.#offset[0] === "-" ? offset : -offset;
|
|
581
|
+
let offsetDate = new Date(this.getTime() - offset * 6e4);
|
|
582
|
+
return offsetDate.toISOString().slice(0, -1) + this.#offset;
|
|
583
|
+
}
|
|
584
|
+
static wrapAsOffsetDateTime(jsDate, offset = "Z") {
|
|
585
|
+
let date = new _TomlDate(jsDate);
|
|
586
|
+
date.#offset = offset;
|
|
587
|
+
return date;
|
|
588
|
+
}
|
|
589
|
+
static wrapAsLocalDateTime(jsDate) {
|
|
590
|
+
let date = new _TomlDate(jsDate);
|
|
591
|
+
date.#offset = null;
|
|
592
|
+
return date;
|
|
593
|
+
}
|
|
594
|
+
static wrapAsLocalDate(jsDate) {
|
|
595
|
+
let date = new _TomlDate(jsDate);
|
|
596
|
+
date.#hasTime = false;
|
|
597
|
+
date.#offset = null;
|
|
598
|
+
return date;
|
|
599
|
+
}
|
|
600
|
+
static wrapAsLocalTime(jsDate) {
|
|
601
|
+
let date = new _TomlDate(jsDate);
|
|
602
|
+
date.#hasDate = false;
|
|
603
|
+
date.#offset = null;
|
|
604
|
+
return date;
|
|
605
|
+
}
|
|
606
|
+
};
|
|
607
|
+
|
|
608
|
+
// ../node_modules/smol-toml/dist/extract.js
|
|
609
|
+
function isDigit(char, base = 10) {
|
|
610
|
+
return base === 16 ? char > 47 && char < 58 || char > 64 && char < 71 || char > 96 && char < 103 : char > 47 && char < 48 + base;
|
|
611
|
+
}
|
|
612
|
+
function isEndOfValue(char, delim) {
|
|
613
|
+
return char === 32 || char === 9 || char === 10 || char === 13 || // Structure end or next value delimiter
|
|
614
|
+
delim && (char === delim || char === 44) || // Comment
|
|
615
|
+
char === 35;
|
|
616
|
+
}
|
|
617
|
+
function extractValue(ctx, end) {
|
|
618
|
+
let errPtr = ctx.p;
|
|
619
|
+
let c = ctx.s.charCodeAt(ctx.p);
|
|
620
|
+
if (c === 91 || c === 123) {
|
|
621
|
+
ctx.d-- || TomlError.x("document contains excessively nested structures. aborting.", ctx);
|
|
622
|
+
let value = c === 91 ? parseArray(ctx) : parseInlineTable(ctx);
|
|
623
|
+
ctx.d++;
|
|
624
|
+
return value;
|
|
625
|
+
}
|
|
626
|
+
if (c === 34 || c === 39) {
|
|
627
|
+
return parseString(ctx);
|
|
628
|
+
}
|
|
629
|
+
if (c === 116) {
|
|
630
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 114 || ctx.s.charCodeAt(++ctx.p) !== 117 || ctx.s.charCodeAt(++ctx.p) !== 101)
|
|
631
|
+
TomlError.x("invalid value", ctx, errPtr);
|
|
632
|
+
return ctx.p++, true;
|
|
633
|
+
}
|
|
634
|
+
if (c === 102) {
|
|
635
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 97 || ctx.s.charCodeAt(++ctx.p) !== 108 || ctx.s.charCodeAt(++ctx.p) !== 115 || ctx.s.charCodeAt(++ctx.p) !== 101)
|
|
636
|
+
TomlError.x("invalid value", ctx, errPtr);
|
|
637
|
+
return ctx.p++, false;
|
|
638
|
+
}
|
|
639
|
+
if (c === 43 || c === 45) {
|
|
640
|
+
return parseNumber(ctx, ctx.p, ctx.s.charCodeAt(++ctx.p), 44 - c, end);
|
|
641
|
+
}
|
|
642
|
+
if (ctx.s.charCodeAt(ctx.p + 4) === 45 && ctx.s.charCodeAt(ctx.p + 7) === 45) {
|
|
643
|
+
return parseDate(ctx, c, end);
|
|
644
|
+
}
|
|
645
|
+
if (ctx.s.charCodeAt(ctx.p + 2) === 58) {
|
|
646
|
+
return parseTime(ctx, c, end);
|
|
647
|
+
}
|
|
648
|
+
return parseNumber(ctx, ctx.p, c, 0, end);
|
|
649
|
+
}
|
|
650
|
+
function parseNumber(ctx, startPtr, startChr, sign, endChr) {
|
|
651
|
+
let c = startChr;
|
|
652
|
+
let state = 0;
|
|
653
|
+
let hasUnderscores = false;
|
|
654
|
+
if (c === 105) {
|
|
655
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 110 || ctx.s.charCodeAt(++ctx.p) !== 102)
|
|
656
|
+
TomlError.x("invalid value", ctx, startPtr);
|
|
657
|
+
return ctx.p++, (sign || 1) / 0;
|
|
658
|
+
}
|
|
659
|
+
if (c === 110) {
|
|
660
|
+
if (ctx.s.charCodeAt(++ctx.p) !== 97 || ctx.s.charCodeAt(++ctx.p) !== 110)
|
|
661
|
+
TomlError.x("invalid value", ctx, startPtr);
|
|
662
|
+
return ctx.p++, NaN;
|
|
663
|
+
}
|
|
664
|
+
if (c === 48) {
|
|
665
|
+
if (++ctx.p >= ctx.s.length || isEndOfValue(c = ctx.s.charCodeAt(ctx.p), endChr))
|
|
666
|
+
return ctx.bi === true ? 0n : 0;
|
|
667
|
+
if (!sign) {
|
|
668
|
+
if (c === 120)
|
|
669
|
+
return parseIntegerBaseN(ctx, startPtr, 16, endChr);
|
|
670
|
+
else if (c === 98)
|
|
671
|
+
return parseIntegerBaseN(ctx, startPtr, 2, endChr);
|
|
672
|
+
else if (c === 111)
|
|
673
|
+
return parseIntegerBaseN(ctx, startPtr, 8, endChr);
|
|
674
|
+
}
|
|
675
|
+
if (c === 46)
|
|
676
|
+
state = 2;
|
|
677
|
+
else if (c === 101 || c === 69)
|
|
678
|
+
state = 4;
|
|
679
|
+
else
|
|
680
|
+
TomlError.x("illegal leading zero", ctx, startPtr);
|
|
681
|
+
} else if (!isDigit(c))
|
|
682
|
+
TomlError.x("invalid value", ctx, startPtr);
|
|
683
|
+
while (++ctx.p < ctx.s.length && (c = ctx.s.charCodeAt(ctx.p), !isEndOfValue(c, endChr))) {
|
|
684
|
+
if (!state)
|
|
685
|
+
state = 1;
|
|
686
|
+
if (c === 95) {
|
|
687
|
+
if (!(state & 1))
|
|
688
|
+
TomlError.x("illegal underscore", ctx);
|
|
689
|
+
state += 11;
|
|
690
|
+
hasUnderscores = true;
|
|
691
|
+
} else if (state === 1 && c === 46)
|
|
692
|
+
state = 2;
|
|
693
|
+
else if ((state === 1 || state === 3) && (c === 101 || c === 69))
|
|
694
|
+
state = 4;
|
|
695
|
+
else if (state === 4 && (c === 43 || c === 45)) {
|
|
696
|
+
state = 6;
|
|
697
|
+
} else if (!isDigit(c))
|
|
698
|
+
TomlError.x(`illegal character in numeric literal`, ctx);
|
|
699
|
+
else if (state > 9)
|
|
700
|
+
state -= 11;
|
|
701
|
+
else if (!(state & 1))
|
|
702
|
+
state++;
|
|
703
|
+
}
|
|
704
|
+
if (!state) {
|
|
705
|
+
let val = (startChr - 48) * (sign || 1);
|
|
706
|
+
return ctx.bi === true ? BigInt(val) : val;
|
|
707
|
+
}
|
|
708
|
+
if (!(state & 1))
|
|
709
|
+
TomlError.x("unfinished numeric value", ctx, startPtr);
|
|
710
|
+
let str = ctx.s.slice(startPtr, ctx.p);
|
|
711
|
+
if (hasUnderscores)
|
|
712
|
+
str = str.replaceAll("_", "");
|
|
713
|
+
return state > 1 ? parseFloat(str) : parseInteger(ctx, str, 10, startPtr);
|
|
714
|
+
}
|
|
715
|
+
function parseIntegerBaseN(ctx, startPtr, base, endChr) {
|
|
716
|
+
let c, underscore = 1;
|
|
717
|
+
while (++ctx.p < ctx.s.length && (c = ctx.s.charCodeAt(ctx.p), !isEndOfValue(c, endChr))) {
|
|
718
|
+
if (c === 95) {
|
|
719
|
+
if (underscore & 1)
|
|
720
|
+
TomlError.x("illegal underscore", ctx);
|
|
721
|
+
underscore = 3;
|
|
722
|
+
} else if (!isDigit(c, base))
|
|
723
|
+
TomlError.x(`illegal character in numeric literal`, ctx);
|
|
724
|
+
else if (underscore & 1)
|
|
725
|
+
underscore--;
|
|
726
|
+
}
|
|
727
|
+
if (underscore & 1)
|
|
728
|
+
TomlError.x("unfinished numeric value", ctx);
|
|
729
|
+
let str = ctx.s.slice(startPtr + 2, ctx.p);
|
|
730
|
+
if (underscore)
|
|
731
|
+
str = str.replaceAll("_", "");
|
|
732
|
+
return parseInteger(ctx, str, base, startPtr);
|
|
733
|
+
}
|
|
734
|
+
function parseInteger(ctx, str, base, startPtr) {
|
|
735
|
+
if (ctx.bi !== true)
|
|
736
|
+
int: {
|
|
737
|
+
let val = parseInt(str, base);
|
|
738
|
+
if (!Number.isSafeInteger(val)) {
|
|
739
|
+
if (ctx.bi)
|
|
740
|
+
break int;
|
|
741
|
+
TomlError.x("integer value cannot be represented losslessly", ctx, startPtr);
|
|
742
|
+
}
|
|
743
|
+
return val;
|
|
744
|
+
}
|
|
745
|
+
return base === 10 ? BigInt(str) : BigInt((base === 2 ? "0b" : base === 8 ? "0o" : "0x") + str);
|
|
746
|
+
}
|
|
747
|
+
function parseDate(ctx, c, endChr) {
|
|
748
|
+
let startPtr = ctx.p++, unsafeSeparator;
|
|
749
|
+
if (!isDigit(c) || !isDigit(ctx.s.charCodeAt(ctx.p++)) || !isDigit(ctx.s.charCodeAt(ctx.p++)) || !isDigit(ctx.s.charCodeAt(ctx.p++))) {
|
|
750
|
+
return parseNumber(ctx, ctx.p = startPtr, c, 0, endChr);
|
|
751
|
+
}
|
|
752
|
+
ctx.p += 5;
|
|
753
|
+
if (!isDigit(ctx.s.charCodeAt(ctx.p++)))
|
|
754
|
+
TomlError.x("invalid date-time: date part is malformed", ctx, startPtr);
|
|
755
|
+
if (ctx.p >= ctx.s.length || ((c = ctx.s.charCodeAt(ctx.p)) !== 32 || (unsafeSeparator = true, !isDigit(ctx.s.charCodeAt(ctx.p + 1)))) && c !== 84 && c !== 116) {
|
|
756
|
+
let t2 = ctx.s.slice(startPtr, ctx.p);
|
|
757
|
+
return readDate(ctx, t2, 3, false, startPtr);
|
|
758
|
+
}
|
|
759
|
+
if (ctx.s.charCodeAt(ctx.p += 3) !== 58)
|
|
760
|
+
TomlError.x("invalid date-time: time part is malformed", ctx, startPtr);
|
|
761
|
+
if (ctx.s.charCodeAt(ctx.p += 3) === 58)
|
|
762
|
+
ctx.p += 3;
|
|
763
|
+
if (ctx.s.charCodeAt(ctx.p) === 46)
|
|
764
|
+
while (isDigit(ctx.s.charCodeAt(++ctx.p)))
|
|
765
|
+
;
|
|
766
|
+
if (c = ctx.s.charCodeAt(ctx.p)) {
|
|
767
|
+
if (c === 90 || c === 122) {
|
|
768
|
+
let t2 = ctx.s.slice(startPtr, ++ctx.p);
|
|
769
|
+
return readDate(ctx, t2, 1, unsafeSeparator, startPtr, "[+00:00]");
|
|
770
|
+
}
|
|
771
|
+
if (c === 43 || c === 45) {
|
|
772
|
+
let t2 = ctx.s.slice(startPtr, ctx.p += 6);
|
|
773
|
+
return readDate(ctx, t2, 1, unsafeSeparator, startPtr, !ctx.ld && "[" + ctx.s.slice(ctx.p - 6, ctx.p) + "]");
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
let t = ctx.s.slice(startPtr, ctx.p);
|
|
777
|
+
return readDate(ctx, t, 2, unsafeSeparator, startPtr);
|
|
778
|
+
}
|
|
779
|
+
function parseTime(ctx, c, endChr) {
|
|
780
|
+
let start = ctx.p;
|
|
781
|
+
if (!isDigit(c) || !isDigit(ctx.s.charCodeAt(++ctx.p))) {
|
|
782
|
+
return parseNumber(ctx, --ctx.p, c, 0, endChr);
|
|
783
|
+
}
|
|
784
|
+
if (ctx.s.charCodeAt(ctx.p += 4) === 58)
|
|
785
|
+
ctx.p += 3;
|
|
786
|
+
if (ctx.s.charCodeAt(ctx.p) === 46)
|
|
787
|
+
while (isDigit(ctx.s.charCodeAt(++ctx.p)))
|
|
788
|
+
;
|
|
789
|
+
let t = ctx.s.slice(start, ctx.p);
|
|
790
|
+
return readDate(ctx, t, 4, false, start);
|
|
791
|
+
}
|
|
792
|
+
function readDate(ctx, str, type, unsafeDelim, errPtr, temporalSuffix) {
|
|
793
|
+
if (ctx.ld) {
|
|
794
|
+
let date = new TomlDate(str, type, unsafeDelim);
|
|
795
|
+
if (!date.isValid())
|
|
796
|
+
TomlError.x("invalid date", ctx, errPtr);
|
|
797
|
+
return date;
|
|
798
|
+
}
|
|
799
|
+
try {
|
|
800
|
+
if (temporalSuffix)
|
|
801
|
+
str += temporalSuffix;
|
|
802
|
+
switch (type) {
|
|
803
|
+
case 1:
|
|
804
|
+
return Temporal.ZonedDateTime.from(str);
|
|
805
|
+
case 2:
|
|
806
|
+
return Temporal.PlainDateTime.from(str);
|
|
807
|
+
case 3:
|
|
808
|
+
return Temporal.PlainDate.from(str);
|
|
809
|
+
case 4:
|
|
810
|
+
return Temporal.PlainTime.from(str);
|
|
811
|
+
}
|
|
812
|
+
} catch (e) {
|
|
813
|
+
TomlError.x(e instanceof Error ? e.message : "" + e, ctx, errPtr);
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// ../node_modules/smol-toml/dist/util.js
|
|
818
|
+
function skipComment(ctx) {
|
|
819
|
+
for (; ctx.p < ctx.s.length; ctx.p++) {
|
|
820
|
+
let c = ctx.s.charCodeAt(ctx.p);
|
|
821
|
+
if (c === 10)
|
|
822
|
+
break;
|
|
823
|
+
if (c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10) {
|
|
824
|
+
ctx.p++;
|
|
825
|
+
break;
|
|
826
|
+
}
|
|
827
|
+
if (c < 32 && c !== 9 || c === 127) {
|
|
828
|
+
TomlError.x("control characters are not allowed in comments", ctx);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
function skipVoid(ctx, banNewLines, banComments) {
|
|
833
|
+
let c;
|
|
834
|
+
while (ctx.p < ctx.s.length) {
|
|
835
|
+
while (ctx.p < ctx.s.length && ((c = ctx.s.charCodeAt(ctx.p)) === 32 || c === 9 || !banNewLines && (c === 10 || c === 13 && ctx.s.charCodeAt(ctx.p + 1) === 10)))
|
|
836
|
+
ctx.p++;
|
|
837
|
+
if (banComments || c !== 35)
|
|
838
|
+
break;
|
|
839
|
+
skipComment(ctx);
|
|
840
|
+
}
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
// ../node_modules/smol-toml/dist/struct.js
|
|
844
|
+
function parseKey(ctx, end = 61) {
|
|
845
|
+
let startPtr;
|
|
846
|
+
let state = 0;
|
|
847
|
+
let parsed = [];
|
|
848
|
+
let sliceStart;
|
|
849
|
+
let c = ctx.s.charCodeAt(startPtr = ctx.p);
|
|
850
|
+
do {
|
|
851
|
+
if (c === end) {
|
|
852
|
+
if (!state)
|
|
853
|
+
TomlError.x("unexpected end of key", ctx);
|
|
854
|
+
if (state === 1)
|
|
855
|
+
parsed.push(ctx.s.slice(sliceStart, ctx.p));
|
|
856
|
+
return ctx.p++, parsed;
|
|
857
|
+
} else if (c === 46) {
|
|
858
|
+
if (!state)
|
|
859
|
+
TomlError.x("illegal empty bare key", ctx);
|
|
860
|
+
if (state === 1)
|
|
861
|
+
parsed.push(ctx.s.slice(sliceStart, ctx.p));
|
|
862
|
+
state = 0;
|
|
863
|
+
} else if (!state && (c === 34 || c === 39)) {
|
|
864
|
+
if (c === ctx.s.charCodeAt(ctx.p + 1) && c === ctx.s.charCodeAt(ctx.p + 2))
|
|
865
|
+
TomlError.x("illegal quoted key: multiline strings are not allowed", ctx);
|
|
866
|
+
parsed.push(parseString(ctx));
|
|
867
|
+
state = 2;
|
|
868
|
+
ctx.p--;
|
|
869
|
+
} else if (c === 32 || c === 9) {
|
|
870
|
+
if (state === 1) {
|
|
871
|
+
parsed.push(ctx.s.slice(sliceStart, ctx.p));
|
|
872
|
+
state = 2;
|
|
873
|
+
}
|
|
874
|
+
} else if (state === 2 || c < 48 && c !== 45 || c > 57 && c < 65 || c > 90 && c < 97 && c !== 95 || c > 122) {
|
|
875
|
+
TomlError.x("illegal character in key", ctx);
|
|
876
|
+
} else if (!state) {
|
|
877
|
+
state = 1;
|
|
878
|
+
sliceStart = ctx.p;
|
|
879
|
+
}
|
|
880
|
+
} while (c = ctx.s.charCodeAt(++ctx.p));
|
|
881
|
+
TomlError.x("incomplete key-value: cannot find end of key", ctx, startPtr);
|
|
882
|
+
}
|
|
883
|
+
function parseInlineTable(ctx) {
|
|
884
|
+
let startPtr = ctx.p++;
|
|
885
|
+
let res = /* @__PURE__ */ Object.create(null);
|
|
886
|
+
let seen = /* @__PURE__ */ new Set();
|
|
887
|
+
let c;
|
|
888
|
+
while (ctx.p < ctx.s.length) {
|
|
889
|
+
skipVoid(ctx);
|
|
890
|
+
if ((c = ctx.s.charCodeAt(ctx.p)) === 125) {
|
|
891
|
+
ctx.p++;
|
|
892
|
+
return res;
|
|
893
|
+
}
|
|
894
|
+
let k;
|
|
895
|
+
let t = res;
|
|
896
|
+
let hasOwn = false;
|
|
897
|
+
let errPtr = ctx.p;
|
|
898
|
+
let key = parseKey(ctx);
|
|
899
|
+
for (let i = 0; i < key.length; i++) {
|
|
900
|
+
if (i)
|
|
901
|
+
t = hasOwn ? t[k] : t[k] = /* @__PURE__ */ Object.create(null);
|
|
902
|
+
k = key[i];
|
|
903
|
+
if ((hasOwn = Object.hasOwn(t, k)) && (typeof t[k] !== "object" || seen.has(t[k]))) {
|
|
904
|
+
TomlError.x("trying to redefine an already defined value", ctx, errPtr);
|
|
905
|
+
}
|
|
906
|
+
let unsafe = k === "__proto__";
|
|
907
|
+
if (ctx.uk && (unsafe || k === "constructor")) {
|
|
908
|
+
t = ctx.uk !== 1 && TomlError.x("document contains an unsafe property", ctx, errPtr);
|
|
909
|
+
break;
|
|
910
|
+
}
|
|
911
|
+
if (!hasOwn && unsafe) {
|
|
912
|
+
Object.defineProperty(t, k, { enumerable: true, configurable: true, writable: true });
|
|
913
|
+
}
|
|
914
|
+
}
|
|
915
|
+
if (hasOwn) {
|
|
916
|
+
TomlError.x("trying to redefine an already defined value", ctx, errPtr);
|
|
917
|
+
}
|
|
918
|
+
skipVoid(ctx, true, true);
|
|
919
|
+
let value = extractValue(
|
|
920
|
+
ctx,
|
|
921
|
+
125
|
|
922
|
+
/* } */
|
|
923
|
+
);
|
|
924
|
+
if (t && typeof (t[k] = value) === "object")
|
|
925
|
+
seen.add(value);
|
|
926
|
+
skipVoid(ctx);
|
|
927
|
+
if ((c = ctx.s.charCodeAt(ctx.p++)) === 125) {
|
|
928
|
+
return res;
|
|
929
|
+
}
|
|
930
|
+
if (c !== 44)
|
|
931
|
+
TomlError.x("expected comma or end of structure", ctx, ctx.p - 1);
|
|
932
|
+
}
|
|
933
|
+
TomlError.x("unfinished table", ctx, startPtr);
|
|
934
|
+
}
|
|
935
|
+
function parseArray(ctx) {
|
|
936
|
+
let startPtr = ctx.p++;
|
|
937
|
+
let res = [];
|
|
938
|
+
let c;
|
|
939
|
+
while (ctx.p < ctx.s.length) {
|
|
940
|
+
skipVoid(ctx);
|
|
941
|
+
if ((c = ctx.s.charCodeAt(ctx.p)) === 93) {
|
|
942
|
+
ctx.p++;
|
|
943
|
+
return res;
|
|
944
|
+
}
|
|
945
|
+
res.push(extractValue(
|
|
946
|
+
ctx,
|
|
947
|
+
93
|
|
948
|
+
/* ] */
|
|
949
|
+
));
|
|
950
|
+
skipVoid(ctx);
|
|
951
|
+
if ((c = ctx.s.charCodeAt(ctx.p++)) === 93) {
|
|
952
|
+
return res;
|
|
953
|
+
}
|
|
954
|
+
if (c !== 44)
|
|
955
|
+
TomlError.x("expected comma or end of structure", ctx, ctx.p - 1);
|
|
956
|
+
}
|
|
957
|
+
TomlError.x("unfinished array", ctx, startPtr);
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// ../node_modules/smol-toml/dist/parse.js
|
|
961
|
+
function peekTable(ctx, key, table, meta, type) {
|
|
962
|
+
let t = table;
|
|
963
|
+
let m = meta;
|
|
964
|
+
let k;
|
|
965
|
+
let hasOwn = false;
|
|
966
|
+
let state;
|
|
967
|
+
for (let i = 0; i < key.length; i++) {
|
|
968
|
+
if (i) {
|
|
969
|
+
t = hasOwn ? t[k] : t[k] = /* @__PURE__ */ Object.create(null);
|
|
970
|
+
m = (state = m[k]).c;
|
|
971
|
+
if (type === 0 && (state.t === 1 || state.t === 2)) {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
if (state.t === 2) {
|
|
975
|
+
let l = t.length - 1;
|
|
976
|
+
t = t[l];
|
|
977
|
+
m = m[l].c;
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
k = key[i];
|
|
981
|
+
if ((hasOwn = Object.hasOwn(t, k)) && m[k]?.t === 0 && m[k]?.d) {
|
|
982
|
+
return null;
|
|
983
|
+
}
|
|
984
|
+
if (!hasOwn) {
|
|
985
|
+
let unsafe = k === "__proto__";
|
|
986
|
+
if (ctx.uk && (unsafe || k === "constructor"))
|
|
987
|
+
return false;
|
|
988
|
+
if (unsafe) {
|
|
989
|
+
Object.defineProperty(t, k, { enumerable: true, configurable: true, writable: true });
|
|
990
|
+
Object.defineProperty(m, k, { enumerable: true, configurable: true, writable: true });
|
|
991
|
+
}
|
|
992
|
+
m[k] = {
|
|
993
|
+
t: i < key.length - 1 && type === 2 ? 3 : type,
|
|
994
|
+
d: false,
|
|
995
|
+
i: 0,
|
|
996
|
+
c: /* @__PURE__ */ Object.create(null)
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
state = m[k];
|
|
1001
|
+
if (state.t !== type && !(type === 1 && state.t === 3)) {
|
|
1002
|
+
return null;
|
|
1003
|
+
}
|
|
1004
|
+
if (type === 2) {
|
|
1005
|
+
if (!state.d) {
|
|
1006
|
+
state.d = true;
|
|
1007
|
+
t[k] = [];
|
|
1008
|
+
}
|
|
1009
|
+
t[k].push(t = /* @__PURE__ */ Object.create(null));
|
|
1010
|
+
state.c[state.i++] = state = { t: 1, d: false, i: 0, c: /* @__PURE__ */ Object.create(null) };
|
|
1011
|
+
}
|
|
1012
|
+
if (state.d) {
|
|
1013
|
+
return null;
|
|
1014
|
+
}
|
|
1015
|
+
state.d = true;
|
|
1016
|
+
if (type === 1) {
|
|
1017
|
+
t = hasOwn ? t[k] : t[k] = /* @__PURE__ */ Object.create(null);
|
|
1018
|
+
} else if (type === 0 && hasOwn) {
|
|
1019
|
+
return null;
|
|
1020
|
+
}
|
|
1021
|
+
return [k, t, state.c];
|
|
1022
|
+
}
|
|
1023
|
+
function validateTablePeek(ctx, peek, ptr) {
|
|
1024
|
+
if (peek === null || ctx.uk === 2)
|
|
1025
|
+
TomlError.x(peek === null ? "trying to redefine an already defined table or value" : "document contains an unsafe property", ctx, ptr);
|
|
1026
|
+
}
|
|
1027
|
+
function parse(toml, options = {}) {
|
|
1028
|
+
let ctx = {
|
|
1029
|
+
s: toml,
|
|
1030
|
+
p: 0,
|
|
1031
|
+
d: options.maxDepth ?? 1e3,
|
|
1032
|
+
bi: options.integersAsBigInt ?? false,
|
|
1033
|
+
ld: options.useLegacyDate ?? true,
|
|
1034
|
+
uk: options.unsafeKeyBehaviour === "throw" ? 2 : options.unsafeKeyBehaviour === "drop" ? 1 : 0
|
|
1035
|
+
};
|
|
1036
|
+
let res = /* @__PURE__ */ Object.create(null);
|
|
1037
|
+
let meta = /* @__PURE__ */ Object.create(null);
|
|
1038
|
+
let tmp;
|
|
1039
|
+
let skipping = false;
|
|
1040
|
+
let tbl = res;
|
|
1041
|
+
let m = meta;
|
|
1042
|
+
if (toml.charCodeAt(0) === 65279)
|
|
1043
|
+
ctx.p++;
|
|
1044
|
+
skipVoid(ctx);
|
|
1045
|
+
while (ctx.p < toml.length) {
|
|
1046
|
+
if (toml.charCodeAt(ctx.p) === 91) {
|
|
1047
|
+
let isTableArray = toml.charCodeAt(++ctx.p) === 91;
|
|
1048
|
+
tmp = ctx.p += +isTableArray;
|
|
1049
|
+
skipping = false;
|
|
1050
|
+
let k = parseKey(
|
|
1051
|
+
ctx,
|
|
1052
|
+
93
|
|
1053
|
+
/* ] */
|
|
1054
|
+
);
|
|
1055
|
+
if (isTableArray) {
|
|
1056
|
+
if (toml.charCodeAt(ctx.p) !== 93) {
|
|
1057
|
+
TomlError.x("expected end of table array declaration", ctx);
|
|
1058
|
+
}
|
|
1059
|
+
ctx.p++;
|
|
1060
|
+
}
|
|
1061
|
+
let p = peekTable(
|
|
1062
|
+
ctx,
|
|
1063
|
+
k,
|
|
1064
|
+
res,
|
|
1065
|
+
meta,
|
|
1066
|
+
isTableArray ? 2 : 1
|
|
1067
|
+
/* Type.EXPLICIT */
|
|
1068
|
+
);
|
|
1069
|
+
if (!p) {
|
|
1070
|
+
validateTablePeek(ctx, p, tmp);
|
|
1071
|
+
skipping = true;
|
|
1072
|
+
} else {
|
|
1073
|
+
m = p[2];
|
|
1074
|
+
tbl = p[1];
|
|
1075
|
+
}
|
|
1076
|
+
} else {
|
|
1077
|
+
tmp = ctx.p;
|
|
1078
|
+
let k = parseKey(ctx);
|
|
1079
|
+
let p = peekTable(
|
|
1080
|
+
ctx,
|
|
1081
|
+
k,
|
|
1082
|
+
tbl,
|
|
1083
|
+
m,
|
|
1084
|
+
0
|
|
1085
|
+
/* Type.DOTTED */
|
|
1086
|
+
);
|
|
1087
|
+
if (!p && !skipping)
|
|
1088
|
+
validateTablePeek(ctx, p, tmp);
|
|
1089
|
+
skipVoid(ctx, true, true);
|
|
1090
|
+
let v = extractValue(ctx, void 0);
|
|
1091
|
+
if (p && !skipping)
|
|
1092
|
+
p[1][p[0]] = v;
|
|
1093
|
+
}
|
|
1094
|
+
skipVoid(ctx, true);
|
|
1095
|
+
if (ctx.p < toml.length && (tmp = toml.charCodeAt(ctx.p)) !== 10 && (tmp !== 13 || toml.charCodeAt(ctx.p + 1) !== 10)) {
|
|
1096
|
+
TomlError.x("each key-value declaration must be followed by an end-of-line", ctx);
|
|
1097
|
+
}
|
|
1098
|
+
skipVoid(ctx);
|
|
1099
|
+
}
|
|
1100
|
+
return res;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
// ../node_modules/smol-toml/dist/stringify.js
|
|
1104
|
+
var HAS_WELLFORMED = !!"".isWellFormed;
|
|
1105
|
+
|
|
1106
|
+
// ../packages/config/src/names.ts
|
|
1107
|
+
var IDENTIFIER = /^[A-Za-z0-9_-]+$/;
|
|
1108
|
+
var ENV_VAR_NAME = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
1109
|
+
var REPO = /^[a-z0-9.-]+(\/[a-z0-9._~-]+)+$/;
|
|
1110
|
+
|
|
1111
|
+
// ../packages/config/src/parse.ts
|
|
1112
|
+
var DEFAULT_CONFIG = {
|
|
1113
|
+
environment: "development",
|
|
1114
|
+
requiredSecrets: []
|
|
1115
|
+
};
|
|
1116
|
+
function parseConfig(text3) {
|
|
1117
|
+
const raw = parse(text3);
|
|
1118
|
+
if (raw.version !== 1) {
|
|
1119
|
+
throw new ConfigError("version must be 1");
|
|
1120
|
+
}
|
|
1121
|
+
const environment = raw.environment ?? DEFAULT_CONFIG.environment;
|
|
1122
|
+
if (typeof environment !== "string" || !IDENTIFIER.test(environment)) {
|
|
1123
|
+
throw new ConfigError('environment must contain only letters, digits, "_" or "-"');
|
|
1124
|
+
}
|
|
1125
|
+
const secrets = raw.secrets ?? {};
|
|
1126
|
+
const required = secrets.required ?? [];
|
|
1127
|
+
if (!Array.isArray(required) || !required.every(isEnvVarName)) {
|
|
1128
|
+
throw new ConfigError("secrets.required must be a list of valid variable names");
|
|
1129
|
+
}
|
|
1130
|
+
return { environment, requiredSecrets: required };
|
|
1131
|
+
}
|
|
1132
|
+
function isEnvVarName(value) {
|
|
1133
|
+
return typeof value === "string" && ENV_VAR_NAME.test(value);
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
// ../packages/config/src/repo.ts
|
|
1137
|
+
var URL_SCHEME = /^[a-z][a-z0-9+.-]*:\/\//i;
|
|
1138
|
+
var SCP_LIKE = /^(?:[^@/]+@)?([^:/]+):(.+)$/;
|
|
1139
|
+
var SUPPORTED_PROTOCOLS = ["https:", "http:", "ssh:", "git:"];
|
|
1140
|
+
function normalizeRemoteUrl(remote) {
|
|
1141
|
+
const [host, path] = splitRemote(remote.trim());
|
|
1142
|
+
const cleanPath = path.replace(/^\/+|\/+$/g, "").replace(/\.git$/, "");
|
|
1143
|
+
const repo = `${host}/${cleanPath}`.toLowerCase();
|
|
1144
|
+
if (!REPO.test(repo)) {
|
|
1145
|
+
throw new ConfigError("git origin is not a supported remote URL");
|
|
1146
|
+
}
|
|
1147
|
+
return repo;
|
|
1148
|
+
}
|
|
1149
|
+
function splitRemote(remote) {
|
|
1150
|
+
if (URL_SCHEME.test(remote)) {
|
|
1151
|
+
const url = URL.parse(remote);
|
|
1152
|
+
if (!url || !SUPPORTED_PROTOCOLS.includes(url.protocol)) {
|
|
1153
|
+
throw new ConfigError("git origin is not a supported remote URL");
|
|
1154
|
+
}
|
|
1155
|
+
return [url.hostname, decodeURIComponent(url.pathname)];
|
|
1156
|
+
}
|
|
1157
|
+
const scp = remote.match(SCP_LIKE);
|
|
1158
|
+
if (!scp) {
|
|
1159
|
+
throw new ConfigError("git origin is not a supported remote URL");
|
|
1160
|
+
}
|
|
1161
|
+
return [scp[1], scp[2]];
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
// ../packages/config/src/git.ts
|
|
1165
|
+
import { execFile } from "node:child_process";
|
|
1166
|
+
import { promisify } from "node:util";
|
|
1167
|
+
var run = promisify(execFile);
|
|
1168
|
+
async function readGitOrigin(dir) {
|
|
1169
|
+
try {
|
|
1170
|
+
const root = await git(dir, "rev-parse", "--show-toplevel");
|
|
1171
|
+
const url = await git(dir, "config", "--get", "remote.origin.url");
|
|
1172
|
+
return { root, url };
|
|
1173
|
+
} catch {
|
|
1174
|
+
return null;
|
|
1175
|
+
}
|
|
1176
|
+
}
|
|
1177
|
+
async function git(dir, ...args2) {
|
|
1178
|
+
const { stdout } = await run("git", ["-C", dir, ...args2]);
|
|
1179
|
+
return stdout.trim();
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
// ../packages/config/src/find.ts
|
|
1183
|
+
import { readFile as readFile2 } from "node:fs/promises";
|
|
1184
|
+
import { join as join4 } from "node:path";
|
|
1185
|
+
var CONFIG_FILE = ".noenv";
|
|
1186
|
+
async function findProject(startDir) {
|
|
1187
|
+
const origin = await readGitOrigin(startDir);
|
|
1188
|
+
if (!origin) return null;
|
|
1189
|
+
const text3 = await readConfigFile(join4(origin.root, CONFIG_FILE));
|
|
1190
|
+
const config = text3 === null ? DEFAULT_CONFIG : parseConfig(text3);
|
|
1191
|
+
return { root: origin.root, repo: normalizeRemoteUrl(origin.url), hasConfig: text3 !== null, ...config };
|
|
1192
|
+
}
|
|
1193
|
+
async function readConfigFile(path) {
|
|
1194
|
+
try {
|
|
1195
|
+
return await readFile2(path, "utf8");
|
|
1196
|
+
} catch (error) {
|
|
1197
|
+
const code = error.code;
|
|
1198
|
+
if (code === "ENOENT" || code === "EISDIR") return null;
|
|
1199
|
+
throw error;
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
// src/target.ts
|
|
1204
|
+
async function findTarget(cwd, env) {
|
|
1205
|
+
const project = await findProject(cwd);
|
|
1206
|
+
if (!project) return null;
|
|
1207
|
+
const environment = env.NOENV_ENVIRONMENT || project.environment;
|
|
1208
|
+
if (!IDENTIFIER.test(environment)) {
|
|
1209
|
+
throw new Error('NOENV_ENVIRONMENT must contain only letters, digits, "_" or "-"');
|
|
1210
|
+
}
|
|
1211
|
+
return { ...project, environment };
|
|
1212
|
+
}
|
|
1213
|
+
async function requireTarget(cwd = process.cwd(), env = process.env) {
|
|
1214
|
+
const target = await findTarget(cwd, env);
|
|
1215
|
+
if (!target) {
|
|
1216
|
+
throw new Error("this directory is not inside a git repository with an origin remote");
|
|
1217
|
+
}
|
|
1218
|
+
return target;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
// src/context.ts
|
|
1222
|
+
function requireDeployment() {
|
|
1223
|
+
const deployment = currentDeployment();
|
|
1224
|
+
if (!deployment) {
|
|
1225
|
+
throw new Error("NOENV_URL is not set, add the lines from your noenv landing page to your shell profile");
|
|
1226
|
+
}
|
|
1227
|
+
return deployment;
|
|
1228
|
+
}
|
|
1229
|
+
async function signedIn() {
|
|
1230
|
+
const deployment = requireDeployment();
|
|
1231
|
+
const token = await requireSession(deployment);
|
|
1232
|
+
return { deployment, token, identity: await ensureIdentity(deployment, token) };
|
|
1233
|
+
}
|
|
1234
|
+
async function inProject() {
|
|
1235
|
+
return { ...await signedIn(), target: await requireTarget() };
|
|
1236
|
+
}
|
|
1237
|
+
|
|
1238
|
+
// src/commands/auth.ts
|
|
1239
|
+
async function authCommand(args2) {
|
|
1240
|
+
const deployment = requireDeployment();
|
|
1241
|
+
if (args2.includes("--wait")) {
|
|
1242
|
+
const pending2 = await readPendingSignIn(deployment);
|
|
1243
|
+
if (pending2) await waitForSignIn(deployment, pending2);
|
|
1244
|
+
return;
|
|
1245
|
+
}
|
|
1246
|
+
const { pending, started } = await startSignIn(deployment);
|
|
1247
|
+
console.log(`Open ${pending.url} and confirm the code ${pending.userCode}`);
|
|
1248
|
+
if (started) openBrowser(pending.url);
|
|
1249
|
+
if (!await waitForSignIn(deployment, pending)) {
|
|
1250
|
+
throw new Error("sign-in was not completed");
|
|
1251
|
+
}
|
|
1252
|
+
const me = await api(deployment, "GET", "/v1/me", { token: await loadSession(deployment) });
|
|
1253
|
+
console.log(`Signed in as ${me.email}`);
|
|
1254
|
+
}
|
|
1255
|
+
async function statusCommand() {
|
|
1256
|
+
const deployment = requireDeployment();
|
|
1257
|
+
console.log(`Deployment: ${deployment.url}`);
|
|
1258
|
+
const token = await loadSession(deployment);
|
|
1259
|
+
if (!token) {
|
|
1260
|
+
console.log("Signed in: no");
|
|
1261
|
+
return;
|
|
1262
|
+
}
|
|
1263
|
+
const me = await api(deployment, "GET", "/v1/me", { token });
|
|
1264
|
+
console.log(`Signed in: ${me.email}${me.isAdmin ? " (organization admin)" : ""}`);
|
|
1265
|
+
const target = await findTarget(process.cwd(), process.env);
|
|
1266
|
+
if (!target) {
|
|
1267
|
+
console.log("Project: none (not in a git repository with an origin)");
|
|
1268
|
+
return;
|
|
1269
|
+
}
|
|
1270
|
+
console.log(`Project: ${target.repo}`);
|
|
1271
|
+
console.log(`Environment: ${target.environment}`);
|
|
1272
|
+
try {
|
|
1273
|
+
const path = `${environmentPath(target.repo, target.environment)}/secrets`;
|
|
1274
|
+
const { secrets } = await api(deployment, "GET", path, { token });
|
|
1275
|
+
console.log(`Secrets: ${secrets.length}`);
|
|
1276
|
+
} catch {
|
|
1277
|
+
console.log("Secrets: no access");
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
// ../packages/policy/src/defaults.ts
|
|
1282
|
+
var DEFAULT_POLICIES = {
|
|
1283
|
+
dotenv: {
|
|
1284
|
+
mode: "warn",
|
|
1285
|
+
include: [".env", ".env.*"],
|
|
1286
|
+
exclude: [".env.example", ".env.sample", ".env.template", ".env.*.example"]
|
|
1287
|
+
},
|
|
1288
|
+
minCliVersion: null,
|
|
1289
|
+
sessionTtlHours: 168,
|
|
1290
|
+
environmentMinRole: "reader"
|
|
1291
|
+
};
|
|
1292
|
+
|
|
1293
|
+
// ../packages/dotenv/src/policy.ts
|
|
1294
|
+
var DEFAULT_DOTENV_POLICY = DEFAULT_POLICIES.dotenv;
|
|
1295
|
+
|
|
1296
|
+
// ../packages/dotenv/src/find.ts
|
|
1297
|
+
import { opendir } from "node:fs/promises";
|
|
1298
|
+
import { join as join5, matchesGlob } from "node:path";
|
|
1299
|
+
var SKIPPED_DIRECTORIES = /* @__PURE__ */ new Set([".git", "node_modules"]);
|
|
1300
|
+
async function findDotenvFiles(root, policy) {
|
|
1301
|
+
const found = [];
|
|
1302
|
+
await walk(root, policy, found);
|
|
1303
|
+
return found.sort();
|
|
1304
|
+
}
|
|
1305
|
+
async function walk(dir, policy, found) {
|
|
1306
|
+
for await (const entry of await opendir(dir)) {
|
|
1307
|
+
const path = join5(dir, entry.name);
|
|
1308
|
+
if (entry.isFile() && matchesPolicy(entry.name, policy)) {
|
|
1309
|
+
found.push(path);
|
|
1310
|
+
} else if (entry.isDirectory() && !SKIPPED_DIRECTORIES.has(entry.name) && !await isGitRepository(path)) {
|
|
1311
|
+
await walk(path, policy, found);
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
function matchesPolicy(name2, { include, exclude }) {
|
|
1316
|
+
return include.some((pattern) => matchesGlob(name2, pattern)) && !exclude.some((pattern) => matchesGlob(name2, pattern));
|
|
1317
|
+
}
|
|
1318
|
+
async function isGitRepository(dir) {
|
|
1319
|
+
for await (const entry of await opendir(dir)) {
|
|
1320
|
+
if (entry.name === ".git") return true;
|
|
1321
|
+
}
|
|
1322
|
+
return false;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
// ../packages/dotenv/src/file.ts
|
|
1326
|
+
import { lstat, readFile as readFile3, unlink } from "node:fs/promises";
|
|
1327
|
+
import { parseEnv } from "node:util";
|
|
1328
|
+
async function readDotenvFile(path) {
|
|
1329
|
+
await requireRegularFile(path);
|
|
1330
|
+
const content = await readFile3(path, "utf8");
|
|
1331
|
+
const values = parseEnv(content);
|
|
1332
|
+
const invalidNames = Object.keys(values).filter((name2) => !ENV_VAR_NAME.test(name2));
|
|
1333
|
+
return { path, content, values, invalidNames };
|
|
1334
|
+
}
|
|
1335
|
+
async function removeIfUnchanged(file) {
|
|
1336
|
+
await requireRegularFile(file.path);
|
|
1337
|
+
if (await readFile3(file.path, "utf8") !== file.content) return false;
|
|
1338
|
+
await unlink(file.path);
|
|
1339
|
+
return true;
|
|
1340
|
+
}
|
|
1341
|
+
async function requireRegularFile(path) {
|
|
1342
|
+
if (!(await lstat(path)).isFile()) {
|
|
1343
|
+
throw new Error("not a regular file");
|
|
1344
|
+
}
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
// ../packages/dotenv/src/migrate.ts
|
|
1348
|
+
async function migrateDotenvFile(file, store) {
|
|
1349
|
+
const conflicts = [];
|
|
1350
|
+
for (const [name2, value] of Object.entries(file.values)) {
|
|
1351
|
+
const stored = await store.get(name2);
|
|
1352
|
+
if (stored === void 0) {
|
|
1353
|
+
await store.set(name2, value);
|
|
1354
|
+
} else if (stored !== value) {
|
|
1355
|
+
conflicts.push(name2);
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
if (conflicts.length > 0) return { verified: false, conflicts };
|
|
1359
|
+
return { verified: await isFullyStored(file, store), conflicts };
|
|
1360
|
+
}
|
|
1361
|
+
async function isFullyStored(file, store) {
|
|
1362
|
+
for (const [name2, value] of Object.entries(file.values)) {
|
|
1363
|
+
if (await store.get(name2) !== value) return false;
|
|
1364
|
+
}
|
|
1365
|
+
return true;
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// ../packages/dotenv/src/enforce.ts
|
|
1369
|
+
async function enforceDotenvPolicy(root, policy, store) {
|
|
1370
|
+
const paths = await findDotenvFiles(root, policy);
|
|
1371
|
+
if (policy.mode !== "delete") {
|
|
1372
|
+
const files2 = paths.map((path) => ({ path, status: "found", names: [] }));
|
|
1373
|
+
return { blocked: policy.mode !== "warn" && files2.length > 0, files: files2 };
|
|
1374
|
+
}
|
|
1375
|
+
const files = [];
|
|
1376
|
+
for (const path of paths) {
|
|
1377
|
+
files.push(await deleteAfterMigration(path, store));
|
|
1378
|
+
}
|
|
1379
|
+
return { blocked: files.some((file) => file.status !== "deleted"), files };
|
|
1380
|
+
}
|
|
1381
|
+
async function deleteAfterMigration(path, store) {
|
|
1382
|
+
try {
|
|
1383
|
+
const file = await readDotenvFile(path);
|
|
1384
|
+
if (file.invalidNames.length > 0) {
|
|
1385
|
+
return { path, status: "invalid", names: file.invalidNames };
|
|
1386
|
+
}
|
|
1387
|
+
const migration = await migrateDotenvFile(file, store);
|
|
1388
|
+
if (migration.conflicts.length > 0) {
|
|
1389
|
+
return { path, status: "conflict", names: migration.conflicts };
|
|
1390
|
+
}
|
|
1391
|
+
if (!migration.verified) {
|
|
1392
|
+
return { path, status: "failed", names: [] };
|
|
1393
|
+
}
|
|
1394
|
+
return { path, status: await removeIfUnchanged(file) ? "deleted" : "changed", names: [] };
|
|
1395
|
+
} catch {
|
|
1396
|
+
return { path, status: "failed", names: [] };
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
|
|
1400
|
+
// src/commands/migrate.ts
|
|
1401
|
+
import { relative as relative2 } from "node:path";
|
|
1402
|
+
|
|
1403
|
+
// src/bundle.ts
|
|
1404
|
+
async function fetchBundle(deployment, token, privateKey, repo, environment) {
|
|
1405
|
+
const raw = await api(deployment, "GET", `${environmentPath(repo, environment)}/bundle`, { token });
|
|
1406
|
+
const environmentPrivateKey = await unwrapPrivateKey(raw.wrappedKey, privateKey);
|
|
1407
|
+
const secrets = {};
|
|
1408
|
+
for (const { name: name2, sealed } of raw.secrets) {
|
|
1409
|
+
secrets[name2] = await decryptSecret(environmentPrivateKey, sealed, { projectId: raw.projectId, environment, name: name2 });
|
|
1410
|
+
}
|
|
1411
|
+
return {
|
|
1412
|
+
projectId: raw.projectId,
|
|
1413
|
+
revision: raw.revision,
|
|
1414
|
+
policies: raw.policies,
|
|
1415
|
+
environmentPublicKey: await importPublicKey(raw.publicKey),
|
|
1416
|
+
environmentPrivateKey,
|
|
1417
|
+
secrets
|
|
1418
|
+
};
|
|
1419
|
+
}
|
|
1420
|
+
|
|
1421
|
+
// src/compliance.ts
|
|
1422
|
+
import { relative } from "node:path";
|
|
1423
|
+
var DESCRIPTIONS = {
|
|
1424
|
+
found: "found",
|
|
1425
|
+
invalid: "kept, it has invalid variable names",
|
|
1426
|
+
conflict: "kept, its values differ from noenv",
|
|
1427
|
+
failed: "kept, it could not be migrated",
|
|
1428
|
+
migrated: "migrated to noenv",
|
|
1429
|
+
changed: "kept, it changed during migration",
|
|
1430
|
+
deleted: "migrated to noenv and deleted"
|
|
1431
|
+
};
|
|
1432
|
+
function describeOutcome(target, outcome) {
|
|
1433
|
+
const names = outcome.names.length > 0 ? ` (${outcome.names.join(", ")})` : "";
|
|
1434
|
+
return `${relative(target.root, outcome.path)}: ${DESCRIPTIONS[outcome.status]}${names}`;
|
|
1435
|
+
}
|
|
1436
|
+
async function reportCompliance(deployment, token, target, result) {
|
|
1437
|
+
await api(deployment, "POST", `${environmentPath(target.repo, target.environment)}/compliance`, {
|
|
1438
|
+
token,
|
|
1439
|
+
body: {
|
|
1440
|
+
compliant: result.files.every((file) => file.status === "deleted"),
|
|
1441
|
+
files: result.files.map((file) => ({ path: relative(target.root, file.path), status: file.status }))
|
|
1442
|
+
}
|
|
1443
|
+
});
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
// src/prompt.ts
|
|
1447
|
+
import { createInterface } from "node:readline/promises";
|
|
1448
|
+
import { text } from "node:stream/consumers";
|
|
1449
|
+
async function readSecretValue(label) {
|
|
1450
|
+
if (!process.stdin.isTTY) {
|
|
1451
|
+
return (await text(process.stdin)).replace(/\r?\n$/, "");
|
|
1452
|
+
}
|
|
1453
|
+
process.stderr.write(label);
|
|
1454
|
+
process.stdin.setRawMode(true);
|
|
1455
|
+
process.stdin.resume();
|
|
1456
|
+
return new Promise((resolve) => {
|
|
1457
|
+
let value = "";
|
|
1458
|
+
const onData = (chunk) => {
|
|
1459
|
+
for (const char of chunk.toString("utf8")) {
|
|
1460
|
+
if (char === "\r" || char === "\n") {
|
|
1461
|
+
finish();
|
|
1462
|
+
return resolve(value);
|
|
1463
|
+
}
|
|
1464
|
+
if (char === "") {
|
|
1465
|
+
finish();
|
|
1466
|
+
process.exit(130);
|
|
1467
|
+
}
|
|
1468
|
+
value = char === "\x7F" ? value.slice(0, -1) : value + char;
|
|
1469
|
+
}
|
|
1470
|
+
};
|
|
1471
|
+
const finish = () => {
|
|
1472
|
+
process.stdin.off("data", onData);
|
|
1473
|
+
process.stdin.setRawMode(false);
|
|
1474
|
+
process.stdin.pause();
|
|
1475
|
+
process.stderr.write("\n");
|
|
1476
|
+
};
|
|
1477
|
+
process.stdin.on("data", onData);
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
async function confirm(question) {
|
|
1481
|
+
if (!process.stdin.isTTY) return false;
|
|
1482
|
+
const readline = createInterface({ input: process.stdin, output: process.stderr });
|
|
1483
|
+
const answer = await readline.question(`${question} [y/N] `);
|
|
1484
|
+
readline.close();
|
|
1485
|
+
return answer.trim().toLowerCase() === "y";
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
// src/secret-store.ts
|
|
1489
|
+
function remoteSecretStore(deployment, token, identity, repo, environment, initial = null) {
|
|
1490
|
+
let cached = initial;
|
|
1491
|
+
const bundle = async () => cached ??= await fetchBundle(deployment, token, identity.privateKey, repo, environment);
|
|
1492
|
+
return {
|
|
1493
|
+
async get(name2) {
|
|
1494
|
+
return (await bundle()).secrets[name2];
|
|
1495
|
+
},
|
|
1496
|
+
async set(name2, value) {
|
|
1497
|
+
const { environmentPublicKey, projectId } = await bundle();
|
|
1498
|
+
const sealed = await encryptSecret(environmentPublicKey, value, { projectId, environment, name: name2 });
|
|
1499
|
+
const path = `${environmentPath(repo, environment)}/secrets/${name2}`;
|
|
1500
|
+
const { version } = await api(deployment, "PUT", path, { token, body: sealed });
|
|
1501
|
+
cached = null;
|
|
1502
|
+
return version;
|
|
1503
|
+
}
|
|
1504
|
+
};
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
// src/watch.ts
|
|
1508
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
1509
|
+
import { createHash } from "node:crypto";
|
|
1510
|
+
import { mkdir as mkdir2, readFile as readFile4, writeFile as writeFile2 } from "node:fs/promises";
|
|
1511
|
+
import { join as join6 } from "node:path";
|
|
1512
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
1513
|
+
var IDLE_LIMIT_MS = 60 * 60 * 1e3;
|
|
1514
|
+
var CHANGED = "changed";
|
|
1515
|
+
function markerPath(deployment, repo, environment) {
|
|
1516
|
+
const name2 = createHash("sha256").update(`${repo}\0${environment}`).digest("hex").slice(0, 32);
|
|
1517
|
+
return join6(deployment.dir, "revisions", name2);
|
|
1518
|
+
}
|
|
1519
|
+
async function readMarker(path) {
|
|
1520
|
+
return readFile4(path, "utf8").catch(() => "");
|
|
1521
|
+
}
|
|
1522
|
+
async function writeMarker(path, value) {
|
|
1523
|
+
await mkdir2(join6(path, ".."), { recursive: true, mode: 448 });
|
|
1524
|
+
await writeFile2(path, value, { mode: 384 });
|
|
1525
|
+
}
|
|
1526
|
+
async function markChanged(deployment, repo, environment) {
|
|
1527
|
+
await writeMarker(markerPath(deployment, repo, environment), CHANGED);
|
|
1528
|
+
}
|
|
1529
|
+
async function watchEnvironment(deployment, repo, environment) {
|
|
1530
|
+
const list = await readJsonFile(watchListPath(deployment)) ?? {};
|
|
1531
|
+
list[`${repo}\0${environment}`] = { repo, environment, seenAt: Date.now() };
|
|
1532
|
+
await writeJsonFile(watchListPath(deployment), list);
|
|
1533
|
+
}
|
|
1534
|
+
async function startWatcher(deployment) {
|
|
1535
|
+
if (await isWatcherRunning(deployment)) return;
|
|
1536
|
+
spawn2(process.execPath, [...process.execArgv, process.argv[1], "watch"], { detached: true, stdio: "ignore" }).unref();
|
|
1537
|
+
}
|
|
1538
|
+
async function runWatcher(deployment) {
|
|
1539
|
+
try {
|
|
1540
|
+
await writeJsonFile(pidPath(deployment), process.pid, "wx");
|
|
1541
|
+
} catch {
|
|
1542
|
+
if (await isWatcherRunning(deployment)) return;
|
|
1543
|
+
await writeJsonFile(pidPath(deployment), process.pid);
|
|
1544
|
+
}
|
|
1545
|
+
try {
|
|
1546
|
+
while (await checkForUpdates(deployment)) {
|
|
1547
|
+
await new Promise((resolve) => setTimeout(resolve, POLL_INTERVAL_MS));
|
|
1548
|
+
}
|
|
1549
|
+
} finally {
|
|
1550
|
+
await removeFile(pidPath(deployment));
|
|
1551
|
+
}
|
|
1552
|
+
}
|
|
1553
|
+
async function checkForUpdates(deployment) {
|
|
1554
|
+
const token = await loadSession(deployment);
|
|
1555
|
+
const list = await readJsonFile(watchListPath(deployment)) ?? {};
|
|
1556
|
+
const active = Object.values(list).filter((entry) => Date.now() - entry.seenAt < IDLE_LIMIT_MS);
|
|
1557
|
+
if (!token || active.length === 0) return false;
|
|
1558
|
+
await refreshMarkers(deployment, token, active).catch(() => {
|
|
1559
|
+
});
|
|
1560
|
+
return true;
|
|
1561
|
+
}
|
|
1562
|
+
async function refreshMarkers(deployment, token, entries) {
|
|
1563
|
+
const { revisions } = await api(deployment, "POST", "/v1/revisions", {
|
|
1564
|
+
token,
|
|
1565
|
+
body: { environments: entries.map(({ repo, environment }) => ({ repo, environment })) }
|
|
1566
|
+
});
|
|
1567
|
+
for (const { repo, environment, revision } of revisions) {
|
|
1568
|
+
const path = markerPath(deployment, repo, environment);
|
|
1569
|
+
if (await readMarker(path) !== String(revision)) await writeMarker(path, String(revision));
|
|
1570
|
+
}
|
|
1571
|
+
}
|
|
1572
|
+
async function isWatcherRunning(deployment) {
|
|
1573
|
+
const pid = await readJsonFile(pidPath(deployment));
|
|
1574
|
+
if (!pid) return false;
|
|
1575
|
+
try {
|
|
1576
|
+
process.kill(pid, 0);
|
|
1577
|
+
return true;
|
|
1578
|
+
} catch {
|
|
1579
|
+
return false;
|
|
1580
|
+
}
|
|
1581
|
+
}
|
|
1582
|
+
function watchListPath(deployment) {
|
|
1583
|
+
return join6(deployment.dir, "watch.json");
|
|
1584
|
+
}
|
|
1585
|
+
function pidPath(deployment) {
|
|
1586
|
+
return join6(deployment.dir, "watcher.pid");
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
// src/commands/migrate.ts
|
|
1590
|
+
async function migrateCommand() {
|
|
1591
|
+
const { deployment, token, identity, target } = await inProject();
|
|
1592
|
+
const bundle = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
|
|
1593
|
+
const store = remoteSecretStore(deployment, token, identity, target.repo, target.environment, bundle);
|
|
1594
|
+
const paths = await findDotenvFiles(target.root, bundle.policies.dotenv);
|
|
1595
|
+
const outcomes = [];
|
|
1596
|
+
if (paths.length === 0) {
|
|
1597
|
+
console.log("No .env files found");
|
|
1598
|
+
return;
|
|
1599
|
+
}
|
|
1600
|
+
for (const path of paths) {
|
|
1601
|
+
const name2 = relative2(target.root, path);
|
|
1602
|
+
const file = await readDotenvFile(path);
|
|
1603
|
+
if (file.invalidNames.length > 0) {
|
|
1604
|
+
console.log(`${name2}: skipped, invalid names: ${file.invalidNames.join(", ")}`);
|
|
1605
|
+
outcomes.push({ path, status: "invalid", names: file.invalidNames });
|
|
1606
|
+
continue;
|
|
1607
|
+
}
|
|
1608
|
+
const result = await migrateDotenvFile(file, store);
|
|
1609
|
+
if (result.conflicts.length > 0) {
|
|
1610
|
+
console.log(`${name2}: kept, values differ from noenv for ${result.conflicts.join(", ")}`);
|
|
1611
|
+
outcomes.push({ path, status: "conflict", names: result.conflicts });
|
|
1612
|
+
continue;
|
|
1613
|
+
}
|
|
1614
|
+
if (!result.verified) {
|
|
1615
|
+
console.log(`${name2}: kept, could not verify the upload`);
|
|
1616
|
+
outcomes.push({ path, status: "failed", names: [] });
|
|
1617
|
+
continue;
|
|
1618
|
+
}
|
|
1619
|
+
console.log(`${name2}: ${Object.keys(file.values).length} values are stored in noenv`);
|
|
1620
|
+
if (!await confirm(`Delete ${name2}?`)) {
|
|
1621
|
+
outcomes.push({ path, status: "migrated", names: [] });
|
|
1622
|
+
continue;
|
|
1623
|
+
}
|
|
1624
|
+
const deleted = await removeIfUnchanged(file);
|
|
1625
|
+
console.log(deleted ? `${name2}: deleted` : `${name2}: kept, the file changed`);
|
|
1626
|
+
outcomes.push({ path, status: deleted ? "deleted" : "changed", names: [] });
|
|
1627
|
+
}
|
|
1628
|
+
await markChanged(deployment, target.repo, target.environment);
|
|
1629
|
+
await reportCompliance(deployment, token, target, {
|
|
1630
|
+
blocked: outcomes.some((outcome) => outcome.status !== "deleted"),
|
|
1631
|
+
files: outcomes
|
|
1632
|
+
});
|
|
1633
|
+
}
|
|
1634
|
+
|
|
1635
|
+
// src/commands/project.ts
|
|
1636
|
+
import { basename } from "node:path";
|
|
1637
|
+
var ROLES = ["reader", "editor", "admin"];
|
|
1638
|
+
async function initCommand() {
|
|
1639
|
+
const { deployment, token, identity, target } = await inProject();
|
|
1640
|
+
try {
|
|
1641
|
+
await api(deployment, "POST", "/v1/projects", { token, body: { name: basename(target.root), repo: target.repo } });
|
|
1642
|
+
console.log(`Created project ${target.repo}`);
|
|
1643
|
+
} catch (error) {
|
|
1644
|
+
if (!(error instanceof ApiError && error.status === 409)) throw error;
|
|
1645
|
+
}
|
|
1646
|
+
const environmentKeys = await generateKeyPair();
|
|
1647
|
+
const wrappedKey = await wrapPrivateKey(environmentKeys.privateKey, await importPublicKey(identity.publicKey));
|
|
1648
|
+
try {
|
|
1649
|
+
await api(deployment, "POST", `${repoPath(target.repo)}/environments/${target.environment}`, {
|
|
1650
|
+
token,
|
|
1651
|
+
body: { publicKey: await exportPublicKey(environmentKeys.publicKey), wrappedKey }
|
|
1652
|
+
});
|
|
1653
|
+
console.log(`Created environment ${target.environment}`);
|
|
1654
|
+
} catch (error) {
|
|
1655
|
+
if (!(error instanceof ApiError && error.status === 409)) throw error;
|
|
1656
|
+
console.log(`Environment ${target.environment} already exists`);
|
|
1657
|
+
}
|
|
1658
|
+
}
|
|
1659
|
+
async function inviteCommand([email]) {
|
|
1660
|
+
if (!email) throw new Error("usage: noenv invite EMAIL");
|
|
1661
|
+
const { deployment, token } = await signedIn();
|
|
1662
|
+
const invite = await api(deployment, "POST", "/v1/users", { token, body: { email } });
|
|
1663
|
+
console.log(invite.emailed ? `Invite emailed to ${email}. The link, in case they need it:` : `Invited ${email}. Send them this link:`);
|
|
1664
|
+
console.log(` ${invite.inviteUrl}`);
|
|
1665
|
+
}
|
|
1666
|
+
async function grantCommand([email, role]) {
|
|
1667
|
+
if (!email || !ROLES.includes(role ?? "")) {
|
|
1668
|
+
throw new Error("usage: noenv grant EMAIL reader|editor|admin");
|
|
1669
|
+
}
|
|
1670
|
+
const { deployment, token, identity, target } = await inProject();
|
|
1671
|
+
const user = await findUser(deployment, token, email);
|
|
1672
|
+
await api(deployment, "PUT", `${repoPath(target.repo)}/members/${user.id}`, { token, body: { role } });
|
|
1673
|
+
console.log(`${email} is now ${role} of ${target.repo}`);
|
|
1674
|
+
if (!user.publicKey) {
|
|
1675
|
+
console.log(`${email} has not signed in yet. Run this command again after they sign in to share the key.`);
|
|
1676
|
+
return;
|
|
1677
|
+
}
|
|
1678
|
+
const bundle = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
|
|
1679
|
+
const wrappedKey = await wrapPrivateKey(bundle.environmentPrivateKey, await importPublicKey(user.publicKey));
|
|
1680
|
+
await api(deployment, "PUT", `${environmentPath(target.repo, target.environment)}/keys/${user.id}`, {
|
|
1681
|
+
token,
|
|
1682
|
+
body: { wrappedKey }
|
|
1683
|
+
});
|
|
1684
|
+
console.log(`Shared the ${target.environment} key with ${email}`);
|
|
1685
|
+
}
|
|
1686
|
+
async function revokeCommand([email]) {
|
|
1687
|
+
if (!email) throw new Error("usage: noenv revoke EMAIL");
|
|
1688
|
+
const { deployment, token, target } = await inProject();
|
|
1689
|
+
const user = await findUser(deployment, token, email);
|
|
1690
|
+
await api(deployment, "DELETE", `${repoPath(target.repo)}/members/${user.id}`, { token });
|
|
1691
|
+
console.log(`Removed ${email} from ${target.repo}. Rotate secrets they could read.`);
|
|
1692
|
+
}
|
|
1693
|
+
async function findUser(deployment, token, email) {
|
|
1694
|
+
return api(deployment, "GET", `/v1/users/lookup?email=${encodeURIComponent(email)}`, { token });
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
// src/commands/secret.ts
|
|
1698
|
+
async function secretCommand([action, ...args2]) {
|
|
1699
|
+
const handlers = {
|
|
1700
|
+
set: setSecret,
|
|
1701
|
+
list: listSecrets,
|
|
1702
|
+
history: secretHistory,
|
|
1703
|
+
rollback: rollbackSecret
|
|
1704
|
+
};
|
|
1705
|
+
const handler = handlers[action ?? ""];
|
|
1706
|
+
if (!handler) {
|
|
1707
|
+
throw new Error("usage: noenv secret set|list|history|rollback");
|
|
1708
|
+
}
|
|
1709
|
+
await handler(args2);
|
|
1710
|
+
}
|
|
1711
|
+
async function setSecret([name2]) {
|
|
1712
|
+
requireName(name2);
|
|
1713
|
+
const { deployment, token, identity, target } = await inProject();
|
|
1714
|
+
const value = await readSecretValue(`Value for ${name2}: `);
|
|
1715
|
+
if (value === "") {
|
|
1716
|
+
throw new Error("the value is empty, nothing was saved");
|
|
1717
|
+
}
|
|
1718
|
+
const store = remoteSecretStore(deployment, token, identity, target.repo, target.environment);
|
|
1719
|
+
const version = await store.set(name2, value);
|
|
1720
|
+
await markChanged(deployment, target.repo, target.environment);
|
|
1721
|
+
console.log(`Saved ${name2} as version ${version} in ${target.repo} (${target.environment})`);
|
|
1722
|
+
}
|
|
1723
|
+
async function listSecrets() {
|
|
1724
|
+
const { deployment, token, target } = await inProject();
|
|
1725
|
+
const { secrets } = await api(
|
|
1726
|
+
deployment,
|
|
1727
|
+
"GET",
|
|
1728
|
+
`${environmentPath(target.repo, target.environment)}/secrets`,
|
|
1729
|
+
{ token }
|
|
1730
|
+
);
|
|
1731
|
+
for (const secret of secrets) {
|
|
1732
|
+
console.log(`${secret.name} v${secret.version} ${secret.updatedAt} ${secret.author}`);
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
async function secretHistory([name2]) {
|
|
1736
|
+
requireName(name2);
|
|
1737
|
+
const { deployment, token, target } = await inProject();
|
|
1738
|
+
const { versions } = await api(
|
|
1739
|
+
deployment,
|
|
1740
|
+
"GET",
|
|
1741
|
+
`${environmentPath(target.repo, target.environment)}/secrets/${name2}/versions`,
|
|
1742
|
+
{ token }
|
|
1743
|
+
);
|
|
1744
|
+
for (const version of versions) {
|
|
1745
|
+
const restored = version.restoredFrom ? ` restored from v${version.restoredFrom}` : "";
|
|
1746
|
+
console.log(`v${version.version} ${version.createdAt} ${version.author}${restored}`);
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
async function rollbackSecret([name2, version]) {
|
|
1750
|
+
requireName(name2);
|
|
1751
|
+
if (!/^\d+$/.test(version ?? "")) {
|
|
1752
|
+
throw new Error("usage: noenv secret rollback KEY VERSION");
|
|
1753
|
+
}
|
|
1754
|
+
const { deployment, token, target } = await inProject();
|
|
1755
|
+
const result = await api(
|
|
1756
|
+
deployment,
|
|
1757
|
+
"POST",
|
|
1758
|
+
`${environmentPath(target.repo, target.environment)}/secrets/${name2}/rollback`,
|
|
1759
|
+
{ token, body: { version: Number(version) } }
|
|
1760
|
+
);
|
|
1761
|
+
await markChanged(deployment, target.repo, target.environment);
|
|
1762
|
+
console.log(`Restored ${name2} v${version} as v${result.version}`);
|
|
1763
|
+
}
|
|
1764
|
+
function requireName(name2) {
|
|
1765
|
+
if (!name2 || !ENV_VAR_NAME.test(name2)) {
|
|
1766
|
+
throw new Error("the secret name must be a valid environment variable name");
|
|
1767
|
+
}
|
|
1768
|
+
}
|
|
1769
|
+
|
|
1770
|
+
// src/commands/shell.ts
|
|
1771
|
+
import { text as text2 } from "node:stream/consumers";
|
|
1772
|
+
|
|
1773
|
+
// src/shell.ts
|
|
1774
|
+
var HOOKS = {
|
|
1775
|
+
zsh: `_noenv_hook() {
|
|
1776
|
+
local revision=""
|
|
1777
|
+
[[ -n "$_NOENV_REV_FILE" && -r "$_NOENV_REV_FILE" ]] && revision="$(<"$_NOENV_REV_FILE")"
|
|
1778
|
+
if [[ "$PWD" != "$_NOENV_PWD" || -n "$_NOENV_RETRY" || "$revision" != "$_NOENV_REV" ]]; then
|
|
1779
|
+
_NOENV_PWD="$PWD"
|
|
1780
|
+
eval "$(printf '%s' "$_NOENV_STATE" | command noenv hook-env zsh)"
|
|
1781
|
+
fi
|
|
1782
|
+
}
|
|
1783
|
+
typeset -ag precmd_functions chpwd_functions
|
|
1784
|
+
if (( ! \${precmd_functions[(I)_noenv_hook]} )); then precmd_functions=(_noenv_hook $precmd_functions); fi
|
|
1785
|
+
if (( ! \${chpwd_functions[(I)_noenv_hook]} )); then chpwd_functions=(_noenv_hook $chpwd_functions); fi
|
|
1786
|
+
`,
|
|
1787
|
+
bash: `_noenv_hook() {
|
|
1788
|
+
local previous_exit=$? revision=""
|
|
1789
|
+
[[ -n "$_NOENV_REV_FILE" && -r "$_NOENV_REV_FILE" ]] && revision="$(<"$_NOENV_REV_FILE")"
|
|
1790
|
+
if [[ "$PWD" != "$_NOENV_PWD" || -n "$_NOENV_RETRY" || "$revision" != "$_NOENV_REV" ]]; then
|
|
1791
|
+
_NOENV_PWD="$PWD"
|
|
1792
|
+
eval "$(printf '%s' "$_NOENV_STATE" | command noenv hook-env bash)"
|
|
1793
|
+
fi
|
|
1794
|
+
return $previous_exit
|
|
1795
|
+
}
|
|
1796
|
+
if [[ ";\${PROMPT_COMMAND:-};" != *";_noenv_hook;"* ]]; then
|
|
1797
|
+
PROMPT_COMMAND="_noenv_hook\${PROMPT_COMMAND:+;$PROMPT_COMMAND}"
|
|
1798
|
+
fi
|
|
1799
|
+
`
|
|
1800
|
+
};
|
|
1801
|
+
function parseShell(value) {
|
|
1802
|
+
if (value !== "zsh" && value !== "bash") {
|
|
1803
|
+
throw new Error("shell must be zsh or bash");
|
|
1804
|
+
}
|
|
1805
|
+
return value;
|
|
1806
|
+
}
|
|
1807
|
+
function hookScript(shell) {
|
|
1808
|
+
return HOOKS[shell];
|
|
1809
|
+
}
|
|
1810
|
+
function quote(value) {
|
|
1811
|
+
return `'${value.replaceAll("'", `'\\''`)}'`;
|
|
1812
|
+
}
|
|
1813
|
+
function exportLine(name2, value) {
|
|
1814
|
+
return `export ${name2}=${quote(value)}`;
|
|
1815
|
+
}
|
|
1816
|
+
function setLine(name2, value) {
|
|
1817
|
+
return `${name2}=${quote(value)}`;
|
|
1818
|
+
}
|
|
1819
|
+
function unsetLine(name2) {
|
|
1820
|
+
return `unset ${name2}`;
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// src/hook-env.ts
|
|
1824
|
+
async function hookEnv(input) {
|
|
1825
|
+
const state = decodeState(input.state);
|
|
1826
|
+
const messages = [];
|
|
1827
|
+
const target = input.deployment ? await findTargetOrReport(input, messages) : null;
|
|
1828
|
+
if (state && target && !state.retry && sameTarget(state, target) && await isCurrent(input, state)) {
|
|
1829
|
+
return { lines: [], messages };
|
|
1830
|
+
}
|
|
1831
|
+
const lines = state ? restoreLines(state) : [];
|
|
1832
|
+
if (!target) {
|
|
1833
|
+
if (state) lines.push(...["_NOENV_STATE", "_NOENV_RETRY", "_NOENV_REV_FILE", "_NOENV_REV"].map(unsetLine));
|
|
1834
|
+
return { lines, messages };
|
|
1835
|
+
}
|
|
1836
|
+
const loaded = await load(input, target, state, messages);
|
|
1837
|
+
lines.push(
|
|
1838
|
+
...loaded.lines,
|
|
1839
|
+
setLine("_NOENV_STATE", encodeState(loaded.state)),
|
|
1840
|
+
loaded.state.retry ? setLine("_NOENV_RETRY", "1") : unsetLine("_NOENV_RETRY"),
|
|
1841
|
+
...revisionLines(input, target, loaded.state.revision)
|
|
1842
|
+
);
|
|
1843
|
+
return { lines, messages };
|
|
1844
|
+
}
|
|
1845
|
+
function sameTarget(state, target) {
|
|
1846
|
+
return state.repo === target.repo && state.environment === target.environment;
|
|
1847
|
+
}
|
|
1848
|
+
async function isCurrent(input, state) {
|
|
1849
|
+
if (state.revision === void 0 || !input.deployment) return true;
|
|
1850
|
+
return await readMarker(markerPath(input.deployment, state.repo, state.environment)) === String(state.revision);
|
|
1851
|
+
}
|
|
1852
|
+
function revisionLines(input, target, revision) {
|
|
1853
|
+
if (revision === void 0 || !input.deployment) return [unsetLine("_NOENV_REV_FILE"), unsetLine("_NOENV_REV")];
|
|
1854
|
+
return [setLine("_NOENV_REV_FILE", markerPath(input.deployment, target.repo, target.environment)), setLine("_NOENV_REV", String(revision))];
|
|
1855
|
+
}
|
|
1856
|
+
async function load(input, target, state, messages) {
|
|
1857
|
+
const { deployment } = input;
|
|
1858
|
+
const empty = { repo: target.repo, environment: target.environment, previous: {}, retry: false };
|
|
1859
|
+
try {
|
|
1860
|
+
const token = await loadSession(deployment);
|
|
1861
|
+
if (!token) {
|
|
1862
|
+
return { lines: [], state: await signIn(input, empty, state, messages) };
|
|
1863
|
+
}
|
|
1864
|
+
const identity = await loadIdentity(deployment) ?? await ensureIdentity(deployment, token);
|
|
1865
|
+
let bundle = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
|
|
1866
|
+
const dotenv = await enforceDotenv({ deployment, token, identity, target, bundle }, messages);
|
|
1867
|
+
if (dotenv.blocked) {
|
|
1868
|
+
return { lines: [], state: empty };
|
|
1869
|
+
}
|
|
1870
|
+
if (dotenv.changed) {
|
|
1871
|
+
bundle = await fetchBundle(deployment, token, identity.privateKey, target.repo, target.environment);
|
|
1872
|
+
}
|
|
1873
|
+
const lines = [];
|
|
1874
|
+
const previous = {};
|
|
1875
|
+
for (const [name2, value] of Object.entries(bundle.secrets)) {
|
|
1876
|
+
previous[name2] = state && name2 in state.previous ? state.previous[name2] : input.env[name2] ?? null;
|
|
1877
|
+
lines.push(exportLine(name2, value));
|
|
1878
|
+
}
|
|
1879
|
+
const reloaded = state !== null && sameTarget(state, target) && state.revision !== void 0;
|
|
1880
|
+
messages.push(`${reloaded ? "updated" : "loaded"} ${lines.length} secrets for ${target.repo} (${target.environment})`);
|
|
1881
|
+
const missing = target.requiredSecrets.filter((name2) => !(name2 in bundle.secrets));
|
|
1882
|
+
if (missing.length > 0) {
|
|
1883
|
+
messages.push(`missing required secrets: ${missing.join(", ")}`);
|
|
1884
|
+
}
|
|
1885
|
+
await writeMarker(markerPath(deployment, target.repo, target.environment), String(bundle.revision));
|
|
1886
|
+
await watchEnvironment(deployment, target.repo, target.environment);
|
|
1887
|
+
input.startWatcher();
|
|
1888
|
+
return { lines, state: { ...empty, previous, revision: bundle.revision } };
|
|
1889
|
+
} catch (error) {
|
|
1890
|
+
if (error instanceof ApiError && error.status === 401) {
|
|
1891
|
+
await clearSession(deployment);
|
|
1892
|
+
return { lines: [], state: { ...empty, retry: true } };
|
|
1893
|
+
}
|
|
1894
|
+
if (!(error instanceof ApiError && error.status === 403 && !target.hasConfig)) {
|
|
1895
|
+
messages.push(`could not load secrets for ${target.repo} (${target.environment}): ${describe(error)}`);
|
|
1896
|
+
}
|
|
1897
|
+
return { lines: [], state: empty };
|
|
1898
|
+
}
|
|
1899
|
+
}
|
|
1900
|
+
async function enforceDotenv(loaded, messages) {
|
|
1901
|
+
const { deployment, token, identity, target, bundle } = loaded;
|
|
1902
|
+
const policy = bundle.policies.dotenv;
|
|
1903
|
+
const store = remoteSecretStore(deployment, token, identity, target.repo, target.environment, bundle);
|
|
1904
|
+
const result = await enforceDotenvPolicy(target.root, policy, store);
|
|
1905
|
+
if (result.files.length === 0) {
|
|
1906
|
+
return { blocked: false, changed: false };
|
|
1907
|
+
}
|
|
1908
|
+
messages.push(...result.files.map((file) => describeOutcome(target, file)));
|
|
1909
|
+
if (result.blocked) {
|
|
1910
|
+
messages.push(`secrets were not loaded: the "${policy.mode}" policy does not allow .env files, run \`noenv migrate\``);
|
|
1911
|
+
}
|
|
1912
|
+
await reportCompliance(deployment, token, target, result).catch(() => messages.push("could not report compliance"));
|
|
1913
|
+
return { blocked: result.blocked, changed: result.files.some((file) => file.status === "deleted") };
|
|
1914
|
+
}
|
|
1915
|
+
async function signIn(input, empty, state, messages) {
|
|
1916
|
+
const waiting = await readPendingSignIn(input.deployment);
|
|
1917
|
+
if (state?.notified && !(waiting && waiting.expiresAt > Date.now())) {
|
|
1918
|
+
messages.push("sign-in was not completed, run `noenv auth` to try again");
|
|
1919
|
+
return empty;
|
|
1920
|
+
}
|
|
1921
|
+
const { pending, started } = await startSignIn(input.deployment);
|
|
1922
|
+
if (started) {
|
|
1923
|
+
input.startPoller();
|
|
1924
|
+
input.openBrowser(pending.url);
|
|
1925
|
+
}
|
|
1926
|
+
if (started || state?.notified !== pending.userCode) {
|
|
1927
|
+
messages.push(`sign in to load secrets: open ${pending.url} and confirm the code ${pending.userCode}`);
|
|
1928
|
+
}
|
|
1929
|
+
return { ...empty, retry: true, notified: pending.userCode };
|
|
1930
|
+
}
|
|
1931
|
+
async function findTargetOrReport(input, messages) {
|
|
1932
|
+
try {
|
|
1933
|
+
return await findTarget(input.cwd, input.env);
|
|
1934
|
+
} catch (error) {
|
|
1935
|
+
messages.push(describe(error));
|
|
1936
|
+
return null;
|
|
1937
|
+
}
|
|
1938
|
+
}
|
|
1939
|
+
function restoreLines(state) {
|
|
1940
|
+
return Object.entries(state.previous).map(([name2, value]) => value === null ? unsetLine(name2) : exportLine(name2, value));
|
|
1941
|
+
}
|
|
1942
|
+
function encodeState(state) {
|
|
1943
|
+
return Buffer.from(JSON.stringify(state)).toString("base64");
|
|
1944
|
+
}
|
|
1945
|
+
function decodeState(text3) {
|
|
1946
|
+
if (!text3.trim()) return null;
|
|
1947
|
+
try {
|
|
1948
|
+
return JSON.parse(Buffer.from(text3.trim(), "base64").toString("utf8"));
|
|
1949
|
+
} catch {
|
|
1950
|
+
return null;
|
|
1951
|
+
}
|
|
1952
|
+
}
|
|
1953
|
+
function describe(error) {
|
|
1954
|
+
return error instanceof Error ? error.message : "unknown error";
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
// src/commands/shell.ts
|
|
1958
|
+
async function shellCommand([shell]) {
|
|
1959
|
+
process.stdout.write(hookScript(parseShell(shell)));
|
|
1960
|
+
}
|
|
1961
|
+
async function hookEnvCommand([shell]) {
|
|
1962
|
+
parseShell(shell);
|
|
1963
|
+
const state = process.stdin.isTTY ? "" : await text2(process.stdin);
|
|
1964
|
+
const deployment = currentDeployment();
|
|
1965
|
+
const { lines, messages } = await hookEnv({
|
|
1966
|
+
cwd: process.cwd(),
|
|
1967
|
+
env: process.env,
|
|
1968
|
+
state,
|
|
1969
|
+
deployment,
|
|
1970
|
+
startPoller: startBackgroundPoller,
|
|
1971
|
+
startWatcher: () => void (deployment && startWatcher(deployment)),
|
|
1972
|
+
openBrowser
|
|
1973
|
+
});
|
|
1974
|
+
for (const message of messages) process.stderr.write(`noenv: ${message}
|
|
1975
|
+
`);
|
|
1976
|
+
process.stdout.write(lines.join("\n"));
|
|
1977
|
+
}
|
|
1978
|
+
async function watchCommand() {
|
|
1979
|
+
await runWatcher(requireDeployment());
|
|
1980
|
+
}
|
|
1981
|
+
|
|
1982
|
+
// src/main.ts
|
|
1983
|
+
var USAGE = `usage: noenv <command>
|
|
1984
|
+
|
|
1985
|
+
shell zsh|bash print the shell hook for your shell profile
|
|
1986
|
+
auth sign in to the deployment in NOENV_URL
|
|
1987
|
+
status show the deployment, user, and project
|
|
1988
|
+
init create the project and environment for this repository
|
|
1989
|
+
secret set KEY save a secret (the value is read without echo)
|
|
1990
|
+
secret list list secrets in this environment
|
|
1991
|
+
secret history KEY list the versions of a secret
|
|
1992
|
+
secret rollback KEY VER restore an older version of a secret
|
|
1993
|
+
migrate move .env files of this repository into noenv
|
|
1994
|
+
invite EMAIL invite a user to this deployment
|
|
1995
|
+
grant EMAIL ROLE give a user access to this project and environment
|
|
1996
|
+
revoke EMAIL remove a user from this project
|
|
1997
|
+
`;
|
|
1998
|
+
var commands = {
|
|
1999
|
+
shell: shellCommand,
|
|
2000
|
+
"hook-env": hookEnvCommand,
|
|
2001
|
+
watch: watchCommand,
|
|
2002
|
+
auth: authCommand,
|
|
2003
|
+
status: statusCommand,
|
|
2004
|
+
init: initCommand,
|
|
2005
|
+
secret: secretCommand,
|
|
2006
|
+
migrate: migrateCommand,
|
|
2007
|
+
invite: inviteCommand,
|
|
2008
|
+
grant: grantCommand,
|
|
2009
|
+
revoke: revokeCommand
|
|
2010
|
+
};
|
|
2011
|
+
var [name, ...args] = process.argv.slice(2);
|
|
2012
|
+
var command = commands[name ?? ""];
|
|
2013
|
+
if (!command) {
|
|
2014
|
+
process.stdout.write(USAGE);
|
|
2015
|
+
process.exit(name ? 1 : 0);
|
|
2016
|
+
}
|
|
2017
|
+
try {
|
|
2018
|
+
await command(args);
|
|
2019
|
+
} catch (error) {
|
|
2020
|
+
process.stderr.write(`noenv: ${error instanceof Error ? error.message : "unknown error"}
|
|
2021
|
+
`);
|
|
2022
|
+
process.exit(1);
|
|
2023
|
+
}
|
|
2024
|
+
/*! Bundled license information:
|
|
2025
|
+
|
|
2026
|
+
smol-toml/dist/error.js:
|
|
2027
|
+
smol-toml/dist/primitive.js:
|
|
2028
|
+
smol-toml/dist/date.js:
|
|
2029
|
+
smol-toml/dist/extract.js:
|
|
2030
|
+
smol-toml/dist/util.js:
|
|
2031
|
+
smol-toml/dist/struct.js:
|
|
2032
|
+
smol-toml/dist/parse.js:
|
|
2033
|
+
smol-toml/dist/stringify.js:
|
|
2034
|
+
smol-toml/dist/index.js:
|
|
2035
|
+
(*!
|
|
2036
|
+
* Copyright (c) Squirrel Chat et al., All rights reserved.
|
|
2037
|
+
* SPDX-License-Identifier: BSD-3-Clause
|
|
2038
|
+
*
|
|
2039
|
+
* Redistribution and use in source and binary forms, with or without
|
|
2040
|
+
* modification, are permitted provided that the following conditions are met:
|
|
2041
|
+
*
|
|
2042
|
+
* 1. Redistributions of source code must retain the above copyright notice, this
|
|
2043
|
+
* list of conditions and the following disclaimer.
|
|
2044
|
+
* 2. Redistributions in binary form must reproduce the above copyright notice,
|
|
2045
|
+
* this list of conditions and the following disclaimer in the
|
|
2046
|
+
* documentation and/or other materials provided with the distribution.
|
|
2047
|
+
* 3. Neither the name of the copyright holder nor the names of its contributors
|
|
2048
|
+
* may be used to endorse or promote products derived from this software without
|
|
2049
|
+
* specific prior written permission.
|
|
2050
|
+
*
|
|
2051
|
+
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
2052
|
+
* ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
2053
|
+
* WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
2054
|
+
* DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
2055
|
+
* FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
2056
|
+
* DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
2057
|
+
* SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
2058
|
+
* CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
2059
|
+
* OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
2060
|
+
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
2061
|
+
*)
|
|
2062
|
+
*/
|