@aprovan/node 0.1.0

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.
@@ -0,0 +1,89 @@
1
+ // src/index.ts
2
+ import { GetParameterCommand, SSMClient } from "@aws-sdk/client-ssm";
3
+
4
+ // src/dotenv.ts
5
+ var LINE_REGEX = /(?:^|^)\s*(?:export\s+)?([\w.-]+)(?:\s*=\s*?|:\s+?)(\s*'(?:\\'|[^'])*'|\s*"(?:\\"|[^"])*"|\s*`(?:\\`|[^`])*`|[^#\r\n]+)?\s*(?:#.*)?(?:$|$)/gm;
6
+ var parse = (src) => {
7
+ const lines = src.toString().replace(/\r\n?/gm, "\n");
8
+ const result = {};
9
+ let match;
10
+ while ((match = LINE_REGEX.exec(lines)) !== null) {
11
+ const key = match[1];
12
+ if (!key) continue;
13
+ let value = (match[2] ?? "").trim();
14
+ const quote = value[0];
15
+ value = value.replace(/^(['"`])([\s\S]*)\1$/gm, "$2");
16
+ if (quote === '"') {
17
+ value = value.replace(/\\n/g, "\n").replace(/\\r/g, "\r");
18
+ }
19
+ result[key] = value;
20
+ }
21
+ return result;
22
+ };
23
+
24
+ // src/index.ts
25
+ var aprovanEnvParameterName = (environment = "prd") => `/aprovan/${environment}/env`;
26
+ async function getAprovanEnv(environment = "prd", client = new SSMClient({})) {
27
+ const response = await client.send(
28
+ new GetParameterCommand({
29
+ Name: aprovanEnvParameterName(environment),
30
+ WithDecryption: true
31
+ })
32
+ );
33
+ const raw = response.Parameter?.Value;
34
+ if (raw === void 0) {
35
+ throw new Error(`Aprovan environment parameter ${environment} has no value`);
36
+ }
37
+ return { raw, values: parse(raw) };
38
+ }
39
+ async function loadAprovanEnv(environment = "prd", options = { overwrite: true }) {
40
+ const { values } = await getAprovanEnv(environment, options.client);
41
+ for (const [key, value] of Object.entries(values)) {
42
+ if (options.overwrite !== false || process.env[key] === void 0) {
43
+ process.env[key] = value;
44
+ }
45
+ }
46
+ return values;
47
+ }
48
+
49
+ export {
50
+ aprovanEnvParameterName,
51
+ getAprovanEnv,
52
+ loadAprovanEnv
53
+ };
54
+ /**
55
+ * Dotenv - Load environment variables from .env files
56
+ *
57
+ * Adapted from dotenv by motdotla
58
+ * @see https://github.com/motdotla/dotenv/blob/master/lib/main.js
59
+ * @license BSD-2-Clause
60
+ *
61
+ * @example
62
+ * // Basic usage - loads from .env in current directory
63
+ * import { config } from './dotenv';
64
+ * config();
65
+ *
66
+ * @example
67
+ * // Custom path
68
+ * config({ path: '/custom/path/.env' });
69
+ *
70
+ * @example
71
+ * // Multiple files (loaded in order, later values don't override)
72
+ * config({ path: ['.env.local', '.env'] });
73
+ *
74
+ * @example
75
+ * // Override existing env vars
76
+ * config({ override: true });
77
+ *
78
+ * @example
79
+ * // Encrypted vault (requires DOTENV_KEY env var)
80
+ * // Automatically loads from .env.vault when DOTENV_KEY is set
81
+ * config();
82
+ *
83
+ * @example
84
+ * // Parse a string directly
85
+ * import { parse } from './dotenv';
86
+ * const env = parse('FOO=bar\nBAZ=qux');
87
+ * // => { FOO: 'bar', BAZ: 'qux' }
88
+ */
89
+ //# sourceMappingURL=chunk-QVRIXCGD.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/dotenv.ts"],"sourcesContent":["import { GetParameterCommand, SSMClient } from \"@aws-sdk/client-ssm\";\nimport { parse } from \"./dotenv\";\n\nexport const aprovanEnvParameterName = (environment = \"prd\"): string =>\n `/aprovan/${environment}/env`;\n\nexport interface LoadAprovanEnvOptions {\n client?: SSMClient;\n overwrite?: boolean;\n}\n\nexport async function getAprovanEnv(\n environment = \"prd\",\n client = new SSMClient({}),\n): Promise<{ raw: string; values: Record<string, string> }> {\n const response = await client.send(\n new GetParameterCommand({\n Name: aprovanEnvParameterName(environment),\n WithDecryption: true,\n }),\n );\n const raw = response.Parameter?.Value;\n if (raw === undefined) {\n throw new Error(`Aprovan environment parameter ${environment} has no value`);\n }\n return { raw, values: parse(raw) };\n}\n\nexport async function loadAprovanEnv(\n environment = \"prd\",\n options: LoadAprovanEnvOptions = { overwrite: true },\n): Promise<Record<string, string>> {\n const { values } = await getAprovanEnv(environment, options.client);\n for (const [key, value] of Object.entries(values)) {\n if (options.overwrite !== false || process.env[key] === undefined) {\n process.env[key] = value;\n }\n }\n return values;\n}\n","/**\n * Dotenv - Load environment variables from .env files\n *\n * Adapted from dotenv by motdotla\n * @see https://github.com/motdotla/dotenv/blob/master/lib/main.js\n * @license BSD-2-Clause\n *\n * @example\n * // Basic usage - loads from .env in current directory\n * import { config } from './dotenv';\n * config();\n *\n * @example\n * // Custom path\n * config({ path: '/custom/path/.env' });\n *\n * @example\n * // Multiple files (loaded in order, later values don't override)\n * config({ path: ['.env.local', '.env'] });\n *\n * @example\n * // Override existing env vars\n * config({ override: true });\n *\n * @example\n * // Encrypted vault (requires DOTENV_KEY env var)\n * // Automatically loads from .env.vault when DOTENV_KEY is set\n * config();\n *\n * @example\n * // Parse a string directly\n * import { parse } from './dotenv';\n * const env = parse('FOO=bar\\nBAZ=qux');\n * // => { FOO: 'bar', BAZ: 'qux' }\n */\n\nimport fs from 'fs';\nimport path from 'path';\nimport os from 'os';\nimport crypto from 'crypto';\n\ntype EnvRecord = Record<string, string>;\ntype ProcessEnv = NodeJS.ProcessEnv | EnvRecord;\n\ninterface DotenvError extends Error {\n code: string;\n}\n\ninterface DotenvOptions {\n path?: string | string[];\n encoding?: BufferEncoding;\n debug?: boolean;\n quiet?: boolean;\n override?: boolean;\n processEnv?: ProcessEnv;\n DOTENV_KEY?: string;\n}\n\ninterface DotenvResult {\n parsed: EnvRecord;\n error?: Error;\n}\n\nconst createError = (message: string, code: string): DotenvError => {\n const err = new Error(message) as DotenvError;\n err.code = code;\n return err;\n};\n\nconst parseBoolean = (value: unknown): boolean =>\n typeof value === 'string'\n ? !['false', '0', 'no', 'off', ''].includes(value.toLowerCase())\n : Boolean(value);\n\nconst supportsAnsi = (): boolean => process.stdout.isTTY;\n\nconst dim = (text: string): string =>\n supportsAnsi() ? `\\x1b[2m${text}\\x1b[0m` : text;\n\nconst log = (msg: string): void => console.log(`[dotenv] ${msg}`);\nconst debug = (msg: string): void => console.debug(`[dotenv:debug] ${msg}`);\nconst warn = (msg: string): void => console.warn(`[dotenv:warn] ${msg}`);\n\nconst resolveHome = (envPath: string): string =>\n envPath.startsWith('~') ? path.join(os.homedir(), envPath.slice(1)) : envPath;\n\nconst LINE_REGEX =\n /(?:^|^)\\s*(?:export\\s+)?([\\w.-]+)(?:\\s*=\\s*?|:\\s+?)(\\s*'(?:\\\\'|[^'])*'|\\s*\"(?:\\\\\"|[^\"])*\"|\\s*`(?:\\\\`|[^`])*`|[^#\\r\\n]+)?\\s*(?:#.*)?(?:$|$)/gm;\n\nconst parse = (src: string | Buffer): EnvRecord => {\n const lines = src.toString().replace(/\\r\\n?/gm, '\\n');\n const result: EnvRecord = {};\n let match: RegExpExecArray | null;\n\n while ((match = LINE_REGEX.exec(lines)) !== null) {\n const key = match[1];\n if (!key) continue;\n\n let value = (match[2] ?? '').trim();\n const quote = value[0];\n\n // Remove surrounding quotes\n value = value.replace(/^(['\"`])([\\s\\S]*)\\1$/gm, '$2');\n\n // Expand escape sequences for double-quoted strings\n if (quote === '\"') {\n value = value.replace(/\\\\n/g, '\\n').replace(/\\\\r/g, '\\r');\n }\n\n result[key] = value;\n }\n\n return result;\n};\n\nconst decrypt = (encrypted: string, keyStr: string): string => {\n const key = Buffer.from(keyStr.slice(-64), 'hex');\n const cipherBuffer = Buffer.from(encrypted, 'base64');\n\n const nonce = cipherBuffer.subarray(0, 12);\n const authTag = cipherBuffer.subarray(-16);\n const ciphertext = cipherBuffer.subarray(12, -16);\n\n try {\n const decipher = crypto.createDecipheriv('aes-256-gcm', key, nonce);\n decipher.setAuthTag(authTag);\n return decipher.update(ciphertext).toString() + decipher.final().toString();\n } catch (error) {\n const err = error as Error;\n const isRange = error instanceof RangeError;\n const invalidKeyLength = err.message === 'Invalid key length';\n const decryptionFailed =\n err.message === 'Unsupported state or unable to authenticate data';\n\n if (isRange || invalidKeyLength) {\n throw createError(\n 'INVALID_DOTENV_KEY: It must be 64 characters long (or more)',\n 'INVALID_DOTENV_KEY',\n );\n }\n if (decryptionFailed) {\n throw createError(\n 'DECRYPTION_FAILED: Please check your DOTENV_KEY',\n 'DECRYPTION_FAILED',\n );\n }\n throw error;\n }\n};\n\nconst populate = (\n target: ProcessEnv,\n source: EnvRecord,\n options: Pick<DotenvOptions, 'debug' | 'override'> = {},\n): EnvRecord => {\n const { debug: showDebug = false, override = false } = options;\n const populated: EnvRecord = {};\n\n if (typeof source !== 'object' || source === null) {\n throw createError(\n 'OBJECT_REQUIRED: Please check the argument being passed to populate',\n 'OBJECT_REQUIRED',\n );\n }\n\n for (const [key, value] of Object.entries(source)) {\n const exists = Object.prototype.hasOwnProperty.call(target, key);\n\n if (exists && !override) {\n if (showDebug) debug(`\"${key}\" already defined, not overwritten`);\n continue;\n }\n\n if (exists && showDebug) {\n debug(`\"${key}\" already defined, overwritten`);\n }\n\n target[key] = value;\n populated[key] = value;\n }\n\n return populated;\n};\n\nconst getDotenvKey = (options?: DotenvOptions): string =>\n options?.DOTENV_KEY || process.env.DOTENV_KEY || '';\n\nconst findVaultPath = (options?: DotenvOptions): string | null => {\n const toVaultPath = (p: string): string =>\n p.endsWith('.vault') ? p : `${p}.vault`;\n\n if (options?.path) {\n const paths = Array.isArray(options.path) ? options.path : [options.path];\n const found = paths.find((p) => fs.existsSync(p));\n if (found) {\n const vaultPath = toVaultPath(found);\n return fs.existsSync(vaultPath) ? vaultPath : null;\n }\n }\n\n const defaultPath = path.resolve(process.cwd(), '.env.vault');\n return fs.existsSync(defaultPath) ? defaultPath : null;\n};\n\nconst parseVaultKey = (\n dotenvKey: string,\n parsed: EnvRecord,\n): { ciphertext: string; key: string } => {\n let uri: URL;\n try {\n uri = new URL(dotenvKey);\n } catch (error) {\n const err = error as NodeJS.ErrnoException;\n if (err.code === 'ERR_INVALID_URL') {\n throw createError(\n 'INVALID_DOTENV_KEY: Wrong format. Must be valid URI like dotenv://:key_1234@dotenvx.com/vault/.env.vault?environment=development',\n 'INVALID_DOTENV_KEY',\n );\n }\n throw error;\n }\n\n const key = uri.password;\n if (!key) {\n throw createError(\n 'INVALID_DOTENV_KEY: Missing key part',\n 'INVALID_DOTENV_KEY',\n );\n }\n\n const environment = uri.searchParams.get('environment');\n if (!environment) {\n throw createError(\n 'INVALID_DOTENV_KEY: Missing environment part',\n 'INVALID_DOTENV_KEY',\n );\n }\n\n const envKey = `DOTENV_VAULT_${environment.toUpperCase()}`;\n const ciphertext = parsed[envKey];\n if (!ciphertext) {\n throw createError(\n `NOT_FOUND_DOTENV_ENVIRONMENT: Cannot locate ${envKey} in .env.vault`,\n 'NOT_FOUND_DOTENV_ENVIRONMENT',\n );\n }\n\n return { ciphertext, key };\n};\n\nconst parseVault = (options: DotenvOptions): EnvRecord => {\n const vaultPath = findVaultPath(options);\n const vaultResult = configDotenv({\n ...options,\n path: vaultPath ?? undefined,\n });\n\n if (!vaultResult.parsed || Object.keys(vaultResult.parsed).length === 0) {\n throw createError(\n `MISSING_DATA: Cannot parse ${vaultPath} for an unknown reason`,\n 'MISSING_DATA',\n );\n }\n\n const keys = getDotenvKey(options).split(',');\n let lastError: Error | null = null;\n\n for (const rawKey of keys) {\n try {\n const { ciphertext, key } = parseVaultKey(\n rawKey.trim(),\n vaultResult.parsed,\n );\n return parse(decrypt(ciphertext, key));\n } catch (error) {\n lastError = error as Error;\n }\n }\n\n throw lastError ?? createError('DECRYPTION_FAILED', 'DECRYPTION_FAILED');\n};\n\nconst configVault = (options: DotenvOptions): DotenvResult => {\n const showDebug = parseBoolean(\n process.env.DOTENV_CONFIG_DEBUG ?? options.debug,\n );\n const quiet = parseBoolean(process.env.DOTENV_CONFIG_QUIET ?? options.quiet);\n\n if (showDebug || !quiet) {\n log('Loading env from encrypted .env.vault');\n }\n\n const parsed = parseVault(options);\n const target = options.processEnv ?? process.env;\n populate(target, parsed, options);\n\n return { parsed };\n};\n\nconst configDotenv = (options?: DotenvOptions): DotenvResult => {\n const target = options?.processEnv ?? process.env;\n const encoding: BufferEncoding = options?.encoding ?? 'utf8';\n let showDebug = parseBoolean(\n target.DOTENV_CONFIG_DEBUG ?? options?.debug ?? false,\n );\n let quiet = parseBoolean(\n target.DOTENV_CONFIG_QUIET ?? options?.quiet ?? false,\n );\n\n const defaultPath = path.resolve(process.cwd(), '.env');\n const paths = options?.path\n ? (Array.isArray(options.path) ? options.path : [options.path]).map(\n resolveHome,\n )\n : [defaultPath];\n\n const parsedAll: EnvRecord = {};\n let lastError: Error | undefined;\n\n for (const filePath of paths) {\n try {\n const content = fs.readFileSync(filePath, { encoding });\n const parsed = parse(content);\n populate(parsedAll, parsed, options);\n } catch (e) {\n if (showDebug)\n debug(`Failed to load ${filePath}: ${(e as Error).message}`);\n lastError = e as Error;\n }\n }\n\n const populated = populate(target, parsedAll, options);\n\n // Re-check settings from loaded .env\n showDebug = parseBoolean(target.DOTENV_CONFIG_DEBUG ?? showDebug);\n quiet = parseBoolean(target.DOTENV_CONFIG_QUIET ?? quiet);\n\n if (showDebug || !quiet) {\n const count = Object.keys(populated).length;\n const shortPaths = paths\n .map((p) => {\n try {\n return path.relative(process.cwd(), p);\n } catch {\n return p;\n }\n })\n .join(', ');\n log(\n `injected ${count} env var(s) from ${shortPaths} ${dim(\n '-- tip: set DOTENV_CONFIG_QUIET=true to silence',\n )}`,\n );\n }\n\n return lastError\n ? { parsed: parsedAll, error: lastError }\n : { parsed: parsedAll };\n};\n\n/** Load environment variables from .env file(s) */\nexport const config = (options?: DotenvOptions): DotenvResult => {\n const dotenvKey = getDotenvKey(options);\n\n if (!dotenvKey) {\n return configDotenv(options);\n }\n\n const vaultPath = findVaultPath(options);\n if (!vaultPath) {\n warn(\n `DOTENV_KEY is set but .env.vault file not found. Did you forget to build it?`,\n );\n return configDotenv(options);\n }\n\n return configVault(options ?? {});\n};\n\nexport { parse, populate, decrypt };\n"],"mappings":";AAAA,SAAS,qBAAqB,iBAAiB;;;ACsF/C,IAAM,aACJ;AAEF,IAAM,QAAQ,CAAC,QAAoC;AACjD,QAAM,QAAQ,IAAI,SAAS,EAAE,QAAQ,WAAW,IAAI;AACpD,QAAM,SAAoB,CAAC;AAC3B,MAAI;AAEJ,UAAQ,QAAQ,WAAW,KAAK,KAAK,OAAO,MAAM;AAChD,UAAM,MAAM,MAAM,CAAC;AACnB,QAAI,CAAC,IAAK;AAEV,QAAI,SAAS,MAAM,CAAC,KAAK,IAAI,KAAK;AAClC,UAAM,QAAQ,MAAM,CAAC;AAGrB,YAAQ,MAAM,QAAQ,0BAA0B,IAAI;AAGpD,QAAI,UAAU,KAAK;AACjB,cAAQ,MAAM,QAAQ,QAAQ,IAAI,EAAE,QAAQ,QAAQ,IAAI;AAAA,IAC1D;AAEA,WAAO,GAAG,IAAI;AAAA,EAChB;AAEA,SAAO;AACT;;;AD9GO,IAAM,0BAA0B,CAAC,cAAc,UACpD,YAAY,WAAW;AAOzB,eAAsB,cACpB,cAAc,OACd,SAAS,IAAI,UAAU,CAAC,CAAC,GACiC;AAC1D,QAAM,WAAW,MAAM,OAAO;AAAA,IAC5B,IAAI,oBAAoB;AAAA,MACtB,MAAM,wBAAwB,WAAW;AAAA,MACzC,gBAAgB;AAAA,IAClB,CAAC;AAAA,EACH;AACA,QAAM,MAAM,SAAS,WAAW;AAChC,MAAI,QAAQ,QAAW;AACrB,UAAM,IAAI,MAAM,iCAAiC,WAAW,eAAe;AAAA,EAC7E;AACA,SAAO,EAAE,KAAK,QAAQ,MAAM,GAAG,EAAE;AACnC;AAEA,eAAsB,eACpB,cAAc,OACd,UAAiC,EAAE,WAAW,KAAK,GAClB;AACjC,QAAM,EAAE,OAAO,IAAI,MAAM,cAAc,aAAa,QAAQ,MAAM;AAClE,aAAW,CAAC,KAAK,KAAK,KAAK,OAAO,QAAQ,MAAM,GAAG;AACjD,QAAI,QAAQ,cAAc,SAAS,QAAQ,IAAI,GAAG,MAAM,QAAW;AACjE,cAAQ,IAAI,GAAG,IAAI;AAAA,IACrB;AAAA,EACF;AACA,SAAO;AACT;","names":[]}
package/dist/cli.d.ts ADDED
@@ -0,0 +1 @@
1
+ #!/usr/bin/env node
package/dist/cli.js ADDED
@@ -0,0 +1,16 @@
1
+ #!/usr/bin/env node
2
+ import {
3
+ getAprovanEnv
4
+ } from "./chunk-QVRIXCGD.js";
5
+
6
+ // src/cli.ts
7
+ var [, , command, environment = "prd"] = process.argv;
8
+ if (command !== "pull") {
9
+ process.stderr.write("Usage: aprovan-env pull [environment]\n");
10
+ process.exitCode = 1;
11
+ } else {
12
+ const { raw } = await getAprovanEnv(environment);
13
+ process.stdout.write(raw.endsWith("\n") ? raw : `${raw}
14
+ `);
15
+ }
16
+ //# sourceMappingURL=cli.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\nimport { getAprovanEnv } from \"./index.js\";\n\nconst [, , command, environment = \"prd\"] = process.argv;\n\nif (command !== \"pull\") {\n process.stderr.write(\"Usage: aprovan-env pull [environment]\\n\");\n process.exitCode = 1;\n} else {\n const { raw } = await getAprovanEnv(environment);\n process.stdout.write(raw.endsWith(\"\\n\") ? raw : `${raw}\\n`);\n}\n"],"mappings":";;;;;;AAGA,IAAM,CAAC,EAAE,EAAE,SAAS,cAAc,KAAK,IAAI,QAAQ;AAEnD,IAAI,YAAY,QAAQ;AACtB,UAAQ,OAAO,MAAM,yCAAyC;AAC9D,UAAQ,WAAW;AACrB,OAAO;AACL,QAAM,EAAE,IAAI,IAAI,MAAM,cAAc,WAAW;AAC/C,UAAQ,OAAO,MAAM,IAAI,SAAS,IAAI,IAAI,MAAM,GAAG,GAAG;AAAA,CAAI;AAC5D;","names":[]}
@@ -0,0 +1,14 @@
1
+ import { SSMClient } from '@aws-sdk/client-ssm';
2
+
3
+ declare const aprovanEnvParameterName: (environment?: string) => string;
4
+ interface LoadAprovanEnvOptions {
5
+ client?: SSMClient;
6
+ overwrite?: boolean;
7
+ }
8
+ declare function getAprovanEnv(environment?: string, client?: SSMClient): Promise<{
9
+ raw: string;
10
+ values: Record<string, string>;
11
+ }>;
12
+ declare function loadAprovanEnv(environment?: string, options?: LoadAprovanEnvOptions): Promise<Record<string, string>>;
13
+
14
+ export { type LoadAprovanEnvOptions, aprovanEnvParameterName, getAprovanEnv, loadAprovanEnv };
package/dist/index.js ADDED
@@ -0,0 +1,11 @@
1
+ import {
2
+ aprovanEnvParameterName,
3
+ getAprovanEnv,
4
+ loadAprovanEnv
5
+ } from "./chunk-QVRIXCGD.js";
6
+ export {
7
+ aprovanEnvParameterName,
8
+ getAprovanEnv,
9
+ loadAprovanEnv
10
+ };
11
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
package/package.json ADDED
@@ -0,0 +1,37 @@
1
+ {
2
+ "name": "@aprovan/node",
3
+ "version": "0.1.0",
4
+ "description": "Shared Aprovan environment loading and utilities",
5
+ "type": "module",
6
+ "bin": {
7
+ "aprovan-env": "./dist/cli.js"
8
+ },
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js"
13
+ }
14
+ },
15
+ "files": [
16
+ "dist"
17
+ ],
18
+ "dependencies": {
19
+ "@aws-sdk/client-ssm": "^3.848.0"
20
+ },
21
+ "devDependencies": {
22
+ "@types/node": "^24.0.13",
23
+ "eslint": "^9.31.0",
24
+ "tsup": "^8.5.0",
25
+ "typescript": "^5.8.3",
26
+ "@aprovan/eslint-config": "1.0.0",
27
+ "@aprovan/tsconfig": "0.1.0"
28
+ },
29
+ "publishConfig": {
30
+ "access": "public"
31
+ },
32
+ "scripts": {
33
+ "build": "tsup",
34
+ "lint": "eslint src",
35
+ "typecheck": "tsc --noEmit"
36
+ }
37
+ }