@rabbitlock/runtime 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.
Binary file
@@ -0,0 +1,30 @@
1
+ /* tslint:disable */
2
+ /* eslint-disable */
3
+ export const memory: WebAssembly.Memory;
4
+ export const greet: (a: number, b: number) => [number, number];
5
+ export const parse_and_verify_sops: (a: number, b: number) => [number, number];
6
+ export const derive_hybrid_keypair: (a: number, b: number) => [number, number, number, number];
7
+ export const verify_sops_integrity: (a: number, b: number, c: number, d: number) => [number, number];
8
+ export const decrypt_data_key: (a: number, b: number, c: number, d: number) => [number, number, number, number];
9
+ export const decrypt_and_verify_sops: (a: number, b: number, c: number, d: number) => [number, number, number, number];
10
+ export const decrypt_sops_content: (a: number, b: number, c: number, d: number) => [number, number, number, number];
11
+ export const encrypt_sops_json: (a: number, b: number, c: number, d: number) => [number, number, number, number];
12
+ export const encrypt_sops_json_for_recipient: (a: number, b: number, c: number, d: number) => [number, number, number, number];
13
+ export const pq_generate_keypair: () => [number, number];
14
+ export const pq_encapsulate: (a: number, b: number) => [number, number, number, number];
15
+ export const pq_decapsulate: (a: number, b: number, c: number, d: number) => [number, number, number, number];
16
+ export const pq_mlkem_generate_keypair: () => [number, number];
17
+ export const pq_mlkem_encapsulate: (a: number, b: number) => [number, number, number, number];
18
+ export const pq_mlkem_decapsulate: (a: number, b: number, c: number, d: number) => [number, number, number, number];
19
+ export const pq_get_sizes: () => [number, number];
20
+ export const create_zip_archive: (a: any) => [number, number, number, number];
21
+ export const encrypt_binary_hybrid: (a: number, b: number, c: number, d: number) => [number, number, number, number];
22
+ export const decrypt_binary_hybrid: (a: number, b: number, c: number, d: number) => [number, number, number, number];
23
+ export const __wbindgen_malloc: (a: number, b: number) => number;
24
+ export const __wbindgen_realloc: (a: number, b: number, c: number, d: number) => number;
25
+ export const __wbindgen_exn_store: (a: number) => void;
26
+ export const __externref_table_alloc: () => number;
27
+ export const __wbindgen_externrefs: WebAssembly.Table;
28
+ export const __wbindgen_free: (a: number, b: number, c: number) => void;
29
+ export const __externref_table_dealloc: (a: number) => void;
30
+ export const __wbindgen_start: () => void;
@@ -0,0 +1,163 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Minimal runtime helper: decrypts a SOPS JSON file with your PQ seed and
4
+ * exports env-style key/value pairs.
5
+ *
6
+ * Usage:
7
+ * rabbitlock-env --seed $RABBITLOCK_SEED_HEX --in env.sops.json --export
8
+ * rabbitlock-env --seed $RABBITLOCK_SEED_HEX --in env.sops.json --json
9
+ * rabbitlock-env --seed $RABBITLOCK_SEED_HEX --in env.sops.json --write-env .env
10
+ */
11
+
12
+ import fs from "fs";
13
+ import path from "path";
14
+ import { fileURLToPath } from "url";
15
+
16
+ const __filename = fileURLToPath(import.meta.url);
17
+ const __dirname = path.dirname(__filename);
18
+
19
+ const args = process.argv.slice(2);
20
+
21
+ const config = {
22
+ seed: process.env.RABBITLOCK_SEED_HEX || "",
23
+ input: "env.sops.json",
24
+ exportEnv: false,
25
+ outputEnvPath: "",
26
+ outputJson: false,
27
+ };
28
+
29
+ for (let i = 0; i < args.length; i++) {
30
+ const arg = args[i];
31
+ if (arg === "--seed") {
32
+ config.seed = args[++i] || "";
33
+ } else if (arg === "--in") {
34
+ config.input = args[++i] || config.input;
35
+ } else if (arg === "--export") {
36
+ config.exportEnv = true;
37
+ } else if (arg === "--write-env") {
38
+ config.outputEnvPath = args[++i] || "";
39
+ } else if (arg === "--json") {
40
+ config.outputJson = true;
41
+ } else if (arg === "--help" || arg === "-h") {
42
+ printHelp();
43
+ process.exit(0);
44
+ }
45
+ }
46
+
47
+ if (!config.seed) {
48
+ console.error(
49
+ "Missing seed. Provide --seed $RABBITLOCK_SEED_HEX or set RABBITLOCK_SEED_HEX.",
50
+ );
51
+ process.exit(1);
52
+ }
53
+
54
+ const wasmPath = path.resolve(__dirname, "./pkg/rabbitlock_crypto.js");
55
+
56
+ const {
57
+ default: initWasm,
58
+ initSync,
59
+ derive_hybrid_keypair,
60
+ decrypt_data_key,
61
+ decrypt_sops_content,
62
+ verify_sops_integrity,
63
+ } = await import(wasmPath);
64
+
65
+ const wasmBytesPath = path.resolve(
66
+ __dirname,
67
+ "./pkg/rabbitlock_crypto_bg.wasm",
68
+ );
69
+
70
+ const loadWasm = async () => {
71
+ // Node: avoid fetch(file://...) by using initSync with local bytes.
72
+ if (typeof window === "undefined") {
73
+ const bytes = fs.readFileSync(wasmBytesPath);
74
+ initSync({ module: bytes });
75
+ } else {
76
+ await initWasm();
77
+ }
78
+ };
79
+
80
+ await loadWasm();
81
+
82
+ const derivePrivateKey = (seedHex) => {
83
+ const kp = derive_hybrid_keypair(seedHex);
84
+ const idx = kp.indexOf(":");
85
+ if (idx === -1) {
86
+ throw new Error("Failed to derive hybrid keypair from seed.");
87
+ }
88
+ return kp.slice(0, idx);
89
+ };
90
+
91
+ const flattenEnv = (value, prefix = "", acc = new Map()) => {
92
+ if (value && typeof value === "object" && !Array.isArray(value)) {
93
+ for (const [k, v] of Object.entries(value)) {
94
+ const nextPrefix = prefix ? `${prefix}_${k}` : k;
95
+ flattenEnv(v, nextPrefix, acc);
96
+ }
97
+ } else if (Array.isArray(value)) {
98
+ value.forEach((v, idx) => flattenEnv(v, `${prefix}_${idx}`, acc));
99
+ } else {
100
+ const key = prefix.toUpperCase();
101
+ const val =
102
+ typeof value === "string"
103
+ ? value
104
+ : value === null
105
+ ? ""
106
+ : JSON.stringify(value);
107
+ acc.set(key, val);
108
+ }
109
+ return acc;
110
+ };
111
+
112
+ const run = () => {
113
+ const inputPath = path.resolve(process.cwd(), config.input);
114
+ const raw = fs.readFileSync(inputPath, "utf8");
115
+
116
+ const privateKey = derivePrivateKey(config.seed.trim());
117
+ const dataKey = decrypt_data_key(raw, privateKey);
118
+ const verified = verify_sops_integrity(raw, dataKey);
119
+ if (!verified.includes("Passed")) {
120
+ throw new Error(`Integrity check failed: ${verified}`);
121
+ }
122
+
123
+ const plaintext = decrypt_sops_content(raw, privateKey);
124
+ const json = JSON.parse(plaintext);
125
+
126
+ if (config.outputJson) {
127
+ console.log(JSON.stringify(json, null, 2));
128
+ }
129
+
130
+ if (config.exportEnv || config.outputEnvPath) {
131
+ const envLines = Array.from(flattenEnv(json).entries()).map(
132
+ ([k, v]) => `${k}=${v}`,
133
+ );
134
+ if (config.outputEnvPath) {
135
+ fs.writeFileSync(config.outputEnvPath, envLines.join("\n"));
136
+ console.error(
137
+ `Wrote ${envLines.length} entries to ${config.outputEnvPath}`,
138
+ );
139
+ } else {
140
+ console.log(envLines.join("\n"));
141
+ }
142
+ }
143
+ };
144
+
145
+ function printHelp() {
146
+ console.log(`RabbitLock env helper
147
+
148
+ Options:
149
+ --seed <hex> RABBITLOCK_SEED_HEX (or set env var)
150
+ --in <path> SOPS JSON file (default: env.sops.json)
151
+ --export Print env-style KEY=value lines
152
+ --write-env <path> Write env lines to a file
153
+ --json Print plaintext JSON
154
+ --help Show this help
155
+ `);
156
+ }
157
+
158
+ try {
159
+ run();
160
+ } catch (err) {
161
+ console.error(err instanceof Error ? err.message : String(err));
162
+ process.exit(1);
163
+ }
@@ -0,0 +1,83 @@
1
+ """
2
+ Lightweight Python shim for RabbitLock SOPS runtime.
3
+
4
+ Requires Node available to execute `rabbitlock-env.mjs` (uses bundled Wasm).
5
+
6
+ Usage:
7
+ from rabbitlock_env import load_env_dict, load_env_into_os
8
+
9
+ env_dict = load_env_dict(seed_hex="...", sops_path="env.sops.json")
10
+ load_env_into_os(seed_hex="...", sops_path="env.sops.json")
11
+ """
12
+
13
+ from __future__ import annotations
14
+
15
+ import json
16
+ import os
17
+ import subprocess
18
+ from pathlib import Path
19
+ from typing import Any, Dict
20
+
21
+ ROOT = Path(__file__).resolve().parent
22
+ NODE_CLI = ROOT / "rabbitlock-env.mjs"
23
+
24
+
25
+ def _flatten_env(
26
+ value: Any, prefix: str = "", acc: Dict[str, str] | None = None
27
+ ) -> Dict[str, str]:
28
+ if acc is None:
29
+ acc = {}
30
+ if isinstance(value, dict):
31
+ for k, v in value.items():
32
+ _flatten_env(v, f"{prefix}_{k}" if prefix else k, acc)
33
+ elif isinstance(value, list):
34
+ for idx, v in enumerate(value):
35
+ _flatten_env(v, f"{prefix}_{idx}", acc)
36
+ else:
37
+ key = prefix.upper()
38
+ acc[key] = value if isinstance(value, str) else json.dumps(value)
39
+ return acc
40
+
41
+
42
+ def _run_node(seed_hex: str, sops_path: str) -> Dict[str, Any]:
43
+ cmd = [
44
+ "node",
45
+ str(NODE_CLI),
46
+ "--seed",
47
+ seed_hex,
48
+ "--in",
49
+ sops_path,
50
+ "--json",
51
+ ]
52
+ result = subprocess.run(
53
+ cmd, check=True, text=True, capture_output=True, cwd=str(ROOT)
54
+ )
55
+ return json.loads(result.stdout)
56
+
57
+
58
+ def load_env_dict(
59
+ seed_hex: str | None = None, sops_path: str | None = None
60
+ ) -> Dict[str, str]:
61
+ """Return a flat dict of env-style key/value pairs from a SOPS file."""
62
+ seed = seed_hex or os.environ.get("RABBITLOCK_SEED_HEX")
63
+ if not seed:
64
+ raise ValueError("Provide seed_hex or set RABBITLOCK_SEED_HEX")
65
+ path = sops_path or os.environ.get("RABBITLOCK_SOPS_PATH") or "env.sops.json"
66
+ plaintext = _run_node(seed, path)
67
+ return _flatten_env(plaintext)
68
+
69
+
70
+ def load_env_into_os(
71
+ seed_hex: str | None = None, sops_path: str | None = None
72
+ ) -> Dict[str, str]:
73
+ """Populate os.environ with decrypted values and return the flat dict."""
74
+ env_dict = load_env_dict(seed_hex, sops_path)
75
+ os.environ.update(env_dict)
76
+ return env_dict
77
+
78
+
79
+ if __name__ == "__main__":
80
+ sops_path_env = os.environ.get("RABBITLOCK_SOPS_PATH")
81
+ data = load_env_into_os(sops_path=sops_path_env)
82
+ for k, v in data.items():
83
+ print(f"{k}={v}")
@@ -0,0 +1,110 @@
1
+ import { test } from "node:test";
2
+ import assert from "node:assert/strict";
3
+ import fs from "node:fs";
4
+ import os from "node:os";
5
+ import path from "node:path";
6
+ import { execFileSync } from "node:child_process";
7
+
8
+ import initWasm, {
9
+ initSync,
10
+ derive_hybrid_keypair,
11
+ encrypt_sops_json_for_recipient,
12
+ } from "./pkg/rabbitlock_crypto.js";
13
+
14
+ const wasmUrl = new URL("./pkg/rabbitlock_crypto_bg.wasm", import.meta.url);
15
+ const wasmBytes = fs.readFileSync(wasmUrl);
16
+ initSync({ module: wasmBytes });
17
+
18
+ const seedHex =
19
+ "1111111111111111111111111111111111111111111111111111111111111111"; // 32 bytes hex
20
+ const cliPath = new URL("./rabbitlock-env.mjs", import.meta.url).pathname;
21
+ const pyPath = new URL("./rabbitlock_env.py", import.meta.url).pathname;
22
+
23
+ const makeTempSops = (plainObj) => {
24
+ const kp = derive_hybrid_keypair(seedHex);
25
+ const [, publicKey] = kp.split(":");
26
+ const sopsJson = encrypt_sops_json_for_recipient(
27
+ JSON.stringify(plainObj),
28
+ publicKey,
29
+ );
30
+
31
+ const dir = fs.mkdtempSync(path.join(os.tmpdir(), "rabbitlock-"));
32
+ const filePath = path.join(dir, "env.sops.json");
33
+ fs.writeFileSync(filePath, sopsJson);
34
+ return { filePath, dir };
35
+ };
36
+
37
+ test("Node CLI --json and --export round-trip env", () => {
38
+ const fixture = { API_KEY: "abc", DB_URL: "postgres://example" };
39
+ const { filePath, dir } = makeTempSops(fixture);
40
+
41
+ const jsonOut = execFileSync(
42
+ "node",
43
+ [cliPath, "--seed", seedHex, "--in", filePath, "--json"],
44
+ { encoding: "utf8" },
45
+ );
46
+ const parsed = JSON.parse(jsonOut);
47
+ assert.deepEqual(parsed, fixture);
48
+
49
+ const exportOut = execFileSync(
50
+ "node",
51
+ [cliPath, "--seed", seedHex, "--in", filePath, "--export"],
52
+ { encoding: "utf8" },
53
+ );
54
+
55
+ const kv = Object.fromEntries(
56
+ exportOut
57
+ .trim()
58
+ .split("\n")
59
+ .map((line) => line.split("=")),
60
+ );
61
+ assert.equal(kv.API_KEY, "abc");
62
+ assert.equal(kv.DB_URL, "postgres://example");
63
+
64
+ fs.rmSync(dir, { recursive: true, force: true });
65
+ });
66
+
67
+ test("Python shim loads env via Node helper", () => {
68
+ const pythonCmd = detectPython();
69
+ if (!pythonCmd) {
70
+ test.skip("python not available");
71
+ return;
72
+ }
73
+
74
+ const fixture = { API_KEY: "xyz", REGION: "us-west-2" };
75
+ const { filePath, dir } = makeTempSops(fixture);
76
+
77
+ const output = execFileSync(pythonCmd, [pyPath], {
78
+ encoding: "utf8",
79
+ env: {
80
+ ...process.env,
81
+ RABBITLOCK_SEED_HEX: seedHex,
82
+ RABBITLOCK_SOPS_PATH: filePath,
83
+ },
84
+ });
85
+
86
+ const kv = Object.fromEntries(
87
+ output
88
+ .trim()
89
+ .split("\n")
90
+ .map((line) => line.split("=")),
91
+ );
92
+
93
+ assert.equal(kv.API_KEY, "xyz");
94
+ assert.equal(kv.REGION, "us-west-2");
95
+
96
+ fs.rmSync(dir, { recursive: true, force: true });
97
+ });
98
+
99
+ function detectPython() {
100
+ const candidates = ["python", "python3"];
101
+ for (const cmd of candidates) {
102
+ try {
103
+ execFileSync(cmd, ["--version"], { stdio: "ignore" });
104
+ return cmd;
105
+ } catch {
106
+ // try next
107
+ }
108
+ }
109
+ return null;
110
+ }