nobalmako 1.0.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.
- package/README.md +112 -0
- package/components.json +22 -0
- package/dist/nobalmako.js +272 -0
- package/drizzle/0000_pink_spiral.sql +126 -0
- package/drizzle/meta/0000_snapshot.json +1027 -0
- package/drizzle/meta/_journal.json +13 -0
- package/drizzle.config.ts +10 -0
- package/eslint.config.mjs +18 -0
- package/next.config.ts +7 -0
- package/package.json +80 -0
- package/postcss.config.mjs +7 -0
- package/public/file.svg +1 -0
- package/public/globe.svg +1 -0
- package/public/next.svg +1 -0
- package/public/vercel.svg +1 -0
- package/public/window.svg +1 -0
- package/server/index.ts +118 -0
- package/src/app/api/api-keys/[id]/route.ts +147 -0
- package/src/app/api/api-keys/route.ts +151 -0
- package/src/app/api/audit-logs/route.ts +84 -0
- package/src/app/api/auth/forgot-password/route.ts +47 -0
- package/src/app/api/auth/login/route.ts +99 -0
- package/src/app/api/auth/logout/route.ts +15 -0
- package/src/app/api/auth/me/route.ts +23 -0
- package/src/app/api/auth/mfa/setup/route.ts +33 -0
- package/src/app/api/auth/mfa/verify/route.ts +45 -0
- package/src/app/api/auth/register/route.ts +140 -0
- package/src/app/api/auth/reset-password/route.ts +52 -0
- package/src/app/api/auth/update/route.ts +71 -0
- package/src/app/api/auth/verify/route.ts +39 -0
- package/src/app/api/environments/route.ts +227 -0
- package/src/app/api/team-members/route.ts +385 -0
- package/src/app/api/teams/route.ts +217 -0
- package/src/app/api/variable-history/route.ts +218 -0
- package/src/app/api/variables/route.ts +476 -0
- package/src/app/api/webhooks/route.ts +77 -0
- package/src/app/api-keys/APIKeysClient.tsx +316 -0
- package/src/app/api-keys/page.tsx +10 -0
- package/src/app/api-reference/page.tsx +324 -0
- package/src/app/audit-log/AuditLogClient.tsx +229 -0
- package/src/app/audit-log/page.tsx +10 -0
- package/src/app/auth/forgot-password/page.tsx +121 -0
- package/src/app/auth/login/LoginForm.tsx +145 -0
- package/src/app/auth/login/page.tsx +11 -0
- package/src/app/auth/register/RegisterForm.tsx +156 -0
- package/src/app/auth/register/page.tsx +16 -0
- package/src/app/auth/reset-password/page.tsx +160 -0
- package/src/app/dashboard/DashboardClient.tsx +219 -0
- package/src/app/dashboard/page.tsx +11 -0
- package/src/app/docs/page.tsx +251 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/globals.css +123 -0
- package/src/app/layout.tsx +35 -0
- package/src/app/page.tsx +231 -0
- package/src/app/profile/ProfileClient.tsx +230 -0
- package/src/app/profile/page.tsx +10 -0
- package/src/app/project/[id]/ProjectDetailsClient.tsx +512 -0
- package/src/app/project/[id]/page.tsx +17 -0
- package/src/bin/nobalmako.ts +341 -0
- package/src/components/ApiKeysManager.tsx +529 -0
- package/src/components/AppLayout.tsx +193 -0
- package/src/components/BulkActions.tsx +138 -0
- package/src/components/CreateEnvironmentDialog.tsx +207 -0
- package/src/components/CreateTeamDialog.tsx +174 -0
- package/src/components/CreateVariableDialog.tsx +311 -0
- package/src/components/DeleteEnvironmentDialog.tsx +104 -0
- package/src/components/DeleteTeamDialog.tsx +112 -0
- package/src/components/DeleteVariableDialog.tsx +103 -0
- package/src/components/EditEnvironmentDialog.tsx +202 -0
- package/src/components/EditMemberDialog.tsx +143 -0
- package/src/components/EditTeamDialog.tsx +178 -0
- package/src/components/EditVariableDialog.tsx +231 -0
- package/src/components/ImportVariablesDialog.tsx +347 -0
- package/src/components/InviteMemberDialog.tsx +191 -0
- package/src/components/LeaveProjectDialog.tsx +111 -0
- package/src/components/MFASettings.tsx +136 -0
- package/src/components/ProjectDiff.tsx +123 -0
- package/src/components/Providers.tsx +24 -0
- package/src/components/RemoveMemberDialog.tsx +112 -0
- package/src/components/SearchDialog.tsx +276 -0
- package/src/components/SecurityOverview.tsx +92 -0
- package/src/components/TeamMembersManager.tsx +103 -0
- package/src/components/VariableHistoryDialog.tsx +265 -0
- package/src/components/WebhooksManager.tsx +169 -0
- package/src/components/ui/alert-dialog.tsx +160 -0
- package/src/components/ui/alert.tsx +59 -0
- package/src/components/ui/avatar.tsx +53 -0
- package/src/components/ui/badge.tsx +46 -0
- package/src/components/ui/button.tsx +62 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/dialog.tsx +143 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sonner.tsx +37 -0
- package/src/components/ui/switch.tsx +31 -0
- package/src/components/ui/table.tsx +117 -0
- package/src/components/ui/tabs.tsx +66 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/hooks/use-api-keys.ts +95 -0
- package/src/hooks/use-audit-logs.ts +58 -0
- package/src/hooks/use-auth.tsx +121 -0
- package/src/hooks/use-environments.ts +33 -0
- package/src/hooks/use-project-permissions.ts +49 -0
- package/src/hooks/use-team-members.ts +30 -0
- package/src/hooks/use-teams.ts +33 -0
- package/src/hooks/use-variables.ts +38 -0
- package/src/lib/audit.ts +36 -0
- package/src/lib/auth.ts +108 -0
- package/src/lib/crypto.ts +39 -0
- package/src/lib/db.ts +15 -0
- package/src/lib/dynamic-providers.ts +19 -0
- package/src/lib/email.ts +110 -0
- package/src/lib/mail.ts +51 -0
- package/src/lib/permissions.ts +51 -0
- package/src/lib/schema.ts +240 -0
- package/src/lib/seed.ts +107 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/webhooks.ts +42 -0
- package/tsconfig.json +34 -0
package/README.md
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Nobalmako - Secure Environment Variable Manager
|
|
2
|
+
|
|
3
|
+
Nobalmako is a full-stack MVP designed to help developers and teams securely store, manage, and share secrets. It features end-to-end encryption for sensitive values and robust role-based access control.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- **End-to-End Security:** All environment variable values are encrypted using AES-256-CBC before storage.
|
|
8
|
+
- **Projects & Environments:** Organize secrets by project and categorize them into `development`, `staging`, or `production`.
|
|
9
|
+
- **Role-Based Access Control (RBAC):**
|
|
10
|
+
- `Owner`: Full control over the project and members.
|
|
11
|
+
- `Admin`: Manage members and secrets.
|
|
12
|
+
- `Developer`: View and manage secrets.
|
|
13
|
+
- `Viewer`: Read-only access to secrets.
|
|
14
|
+
- **Audit Logs:** Track every action performed on your secrets for compliance and security.
|
|
15
|
+
- **API Keys:** Secure programmatic access for CI/CD and CLI integrations.
|
|
16
|
+
- **Variable History:** View and compare previous versions of any secret.
|
|
17
|
+
|
|
18
|
+
## Nobalmako CLI
|
|
19
|
+
|
|
20
|
+
Manage your secrets directly from your terminal or CI/CD pipeline.
|
|
21
|
+
|
|
22
|
+
### Installation
|
|
23
|
+
|
|
24
|
+
For global system-wide access:
|
|
25
|
+
\`bash
|
|
26
|
+
# Build the CLI
|
|
27
|
+
npm run build:cli
|
|
28
|
+
|
|
29
|
+
# Install globally
|
|
30
|
+
npm install -g .
|
|
31
|
+
\`
|
|
32
|
+
|
|
33
|
+
Alternatively, run without installing using \`npx\`:
|
|
34
|
+
\`bash
|
|
35
|
+
# Build first
|
|
36
|
+
npm run build:cli
|
|
37
|
+
|
|
38
|
+
# Run via npx (from project root)
|
|
39
|
+
npx nobalmako --help
|
|
40
|
+
\`
|
|
41
|
+
|
|
42
|
+
### Usage
|
|
43
|
+
|
|
44
|
+
1. **Local Authentication:**
|
|
45
|
+
Authenticate your CLI once and it will remember your session:
|
|
46
|
+
\`bash
|
|
47
|
+
nobalmako login --email dev@nobalmako.com --password "your_password"
|
|
48
|
+
\`
|
|
49
|
+
|
|
50
|
+
Alternatively, use an API Token for CI/CD:
|
|
51
|
+
\`bash
|
|
52
|
+
export NOBALMAKO_TOKEN="nm_your_api_key_here"
|
|
53
|
+
\`
|
|
54
|
+
|
|
55
|
+
2. **Pull Secrets:**
|
|
56
|
+
\`bash
|
|
57
|
+
nobalmako pull -p "Project Name" -e "production" -f ".env"
|
|
58
|
+
\`
|
|
59
|
+
|
|
60
|
+
3. **Push Secrets:**
|
|
61
|
+
\`bash
|
|
62
|
+
nobalmako push -p "Project Name" -e "staging" -f ".env"
|
|
63
|
+
\`
|
|
64
|
+
|
|
65
|
+
## Tech Stack
|
|
66
|
+
|
|
67
|
+
- **Frontend:** Next.js 15, React, Tailwind CSS, Lucide Icons, Radix UI.
|
|
68
|
+
- **Backend:** Node.js, Express (custom server), Next.js API Routes.
|
|
69
|
+
- **Database:** PostgreSQL (using Drizzle ORM).
|
|
70
|
+
- **Authentication:** JWT (JSON Web Tokens).
|
|
71
|
+
|
|
72
|
+
## Setup Instructions
|
|
73
|
+
|
|
74
|
+
### 1. Prerequisites
|
|
75
|
+
- Node.js 18+
|
|
76
|
+
- PostgreSQL database (or a Neon/Supabase project)
|
|
77
|
+
|
|
78
|
+
### 2. Environment Variables
|
|
79
|
+
Copy `.env.example` to `.env` and fill in the values:
|
|
80
|
+
\`bash
|
|
81
|
+
cp .env.example .env
|
|
82
|
+
\`
|
|
83
|
+
|
|
84
|
+
### 3. Installation
|
|
85
|
+
\`bash
|
|
86
|
+
npm install
|
|
87
|
+
\`
|
|
88
|
+
|
|
89
|
+
### 4. Database Setup
|
|
90
|
+
\`bash
|
|
91
|
+
# Push schema to database
|
|
92
|
+
npx drizzle-kit push
|
|
93
|
+
\`
|
|
94
|
+
|
|
95
|
+
### 5. Running the Application
|
|
96
|
+
\`bash
|
|
97
|
+
# Development mode
|
|
98
|
+
npm run dev
|
|
99
|
+
|
|
100
|
+
# Build and Start
|
|
101
|
+
npm run build
|
|
102
|
+
npm start
|
|
103
|
+
\`
|
|
104
|
+
|
|
105
|
+
## Security Model
|
|
106
|
+
|
|
107
|
+
1. **At-Rest:** Secrets are encrypted using AES-256-CBC. Even if the database is compromised, the values remain unreadable without the `ENCRYPTION_KEY`.
|
|
108
|
+
2. **In-Transit:** All requests are handled via HTTPS (recommended for production).
|
|
109
|
+
3. **Application Level:** RBAC is enforced on every API request. A `Viewer` cannot perform `POST/PUT/DELETE` operations on secrets.
|
|
110
|
+
|
|
111
|
+
## License
|
|
112
|
+
MIT
|
package/components.json
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
{
|
|
2
|
+
"$schema": "https://ui.shadcn.com/schema.json",
|
|
3
|
+
"style": "new-york",
|
|
4
|
+
"rsc": true,
|
|
5
|
+
"tsx": true,
|
|
6
|
+
"tailwind": {
|
|
7
|
+
"config": "",
|
|
8
|
+
"css": "src/app/globals.css",
|
|
9
|
+
"baseColor": "neutral",
|
|
10
|
+
"cssVariables": true,
|
|
11
|
+
"prefix": ""
|
|
12
|
+
},
|
|
13
|
+
"iconLibrary": "lucide",
|
|
14
|
+
"aliases": {
|
|
15
|
+
"components": "@/components",
|
|
16
|
+
"utils": "@/lib/utils",
|
|
17
|
+
"ui": "@/components/ui",
|
|
18
|
+
"lib": "@/lib",
|
|
19
|
+
"hooks": "@/hooks"
|
|
20
|
+
},
|
|
21
|
+
"registries": {}
|
|
22
|
+
}
|
|
@@ -0,0 +1,272 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/bin/nobalmako.ts
|
|
27
|
+
var import_commander = require("commander");
|
|
28
|
+
var import_fs = __toESM(require("fs"));
|
|
29
|
+
var import_path = __toESM(require("path"));
|
|
30
|
+
var import_dotenv = __toESM(require("dotenv"));
|
|
31
|
+
var import_os = __toESM(require("os"));
|
|
32
|
+
var import_child_process = require("child_process");
|
|
33
|
+
var import_enquirer = require("enquirer");
|
|
34
|
+
var program = new import_commander.Command();
|
|
35
|
+
var CONFIG_FILE = import_path.default.join(import_os.default.homedir(), ".nobalmako.json");
|
|
36
|
+
var PROJECT_CONFIG = import_path.default.join(process.cwd(), "nobalmako.json");
|
|
37
|
+
function saveConfig(config) {
|
|
38
|
+
const current = loadConfig();
|
|
39
|
+
import_fs.default.writeFileSync(CONFIG_FILE, JSON.stringify({ ...current, ...config }, null, 2));
|
|
40
|
+
}
|
|
41
|
+
function loadConfig() {
|
|
42
|
+
if (import_fs.default.existsSync(CONFIG_FILE)) {
|
|
43
|
+
return JSON.parse(import_fs.default.readFileSync(CONFIG_FILE, "utf-8"));
|
|
44
|
+
}
|
|
45
|
+
return {};
|
|
46
|
+
}
|
|
47
|
+
function loadProjectConfig() {
|
|
48
|
+
if (import_fs.default.existsSync(PROJECT_CONFIG)) {
|
|
49
|
+
return JSON.parse(import_fs.default.readFileSync(PROJECT_CONFIG, "utf-8"));
|
|
50
|
+
}
|
|
51
|
+
return {};
|
|
52
|
+
}
|
|
53
|
+
function getToken(optionsToken) {
|
|
54
|
+
const config = loadConfig();
|
|
55
|
+
return optionsToken || process.env.NOBALMAKO_TOKEN || config.token;
|
|
56
|
+
}
|
|
57
|
+
program.name("nobalmako").description("Securing your environment variables").version("1.0.0");
|
|
58
|
+
program.command("login").description("Login to Nobalmako").option("--email <email>", "User email").option("--password <password>", "User password").option("--api-url <url>", "Base API URL", "http://localhost:3000/api").action(async (options) => {
|
|
59
|
+
let { email, password } = options;
|
|
60
|
+
if (!email) {
|
|
61
|
+
const response = await (0, import_enquirer.prompt)({
|
|
62
|
+
type: "input",
|
|
63
|
+
name: "email",
|
|
64
|
+
message: "Enter your email:"
|
|
65
|
+
});
|
|
66
|
+
email = response.email;
|
|
67
|
+
}
|
|
68
|
+
if (!password) {
|
|
69
|
+
const response = await (0, import_enquirer.prompt)({
|
|
70
|
+
type: "password",
|
|
71
|
+
name: "password",
|
|
72
|
+
message: "Enter your password:"
|
|
73
|
+
});
|
|
74
|
+
password = response.password;
|
|
75
|
+
}
|
|
76
|
+
try {
|
|
77
|
+
const response = await fetch(`${options.apiUrl}/auth/login`, {
|
|
78
|
+
method: "POST",
|
|
79
|
+
headers: { "Content-Type": "application/json" },
|
|
80
|
+
body: JSON.stringify({ email, password })
|
|
81
|
+
});
|
|
82
|
+
if (!response.ok) {
|
|
83
|
+
throw new Error("Login failed. Please check your credentials.");
|
|
84
|
+
}
|
|
85
|
+
const data = await response.json();
|
|
86
|
+
saveConfig({ token: data.token, email: data.user.email });
|
|
87
|
+
console.log(`\x1B[32mSuccessfully logged in as ${data.user.email}!\x1B[0m`);
|
|
88
|
+
} catch (error) {
|
|
89
|
+
console.error(`\x1B[31mError:\x1B[0m ${error.message}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
}
|
|
92
|
+
});
|
|
93
|
+
program.command("logout").description("Clear local credentials").action(() => {
|
|
94
|
+
if (import_fs.default.existsSync(CONFIG_FILE)) {
|
|
95
|
+
import_fs.default.unlinkSync(CONFIG_FILE);
|
|
96
|
+
console.log("\x1B[32mSuccessfully logged out.\x1B[0m");
|
|
97
|
+
} else {
|
|
98
|
+
console.log("You are not logged in.");
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
program.command("init").description("Initialize a local project configuration").option("-p, --project <project>", "Project name").option("-e, --env <environment>", "Default environment name").action(async (options) => {
|
|
102
|
+
let { project, env } = options;
|
|
103
|
+
if (!project) {
|
|
104
|
+
const response = await (0, import_enquirer.prompt)({
|
|
105
|
+
type: "input",
|
|
106
|
+
name: "project",
|
|
107
|
+
message: "Project name (slug):"
|
|
108
|
+
});
|
|
109
|
+
project = response.project;
|
|
110
|
+
}
|
|
111
|
+
if (!env) {
|
|
112
|
+
const response = await (0, import_enquirer.prompt)({
|
|
113
|
+
type: "input",
|
|
114
|
+
name: "env",
|
|
115
|
+
message: "Default environment (e.g. production):"
|
|
116
|
+
});
|
|
117
|
+
env = response.env;
|
|
118
|
+
}
|
|
119
|
+
import_fs.default.writeFileSync(PROJECT_CONFIG, JSON.stringify({ project, environment: env }, null, 2));
|
|
120
|
+
console.log(`\x1B[32mCreated nobalmako.json with defaults.\x1B[0m`);
|
|
121
|
+
});
|
|
122
|
+
program.command("pull").description("Pull environment variables from Nobalmako and save to .env").option("-p, --project <project>", "Project name").option("-e, --env <environment>", "Environment name (e.g. production, staging)").option("-f, --file <filename>", "Output filename").option("--api-url <url>", "Base API URL", "http://localhost:3000/api").option("-t, --token <token>", "API Token (overrides login)").action(async (options) => {
|
|
123
|
+
const token = getToken(options.token);
|
|
124
|
+
const pConfig = loadProjectConfig();
|
|
125
|
+
const project = options.project || pConfig.project;
|
|
126
|
+
const env = options.env || pConfig.environment;
|
|
127
|
+
const file = options.file || pConfig.file || ".env";
|
|
128
|
+
if (!token) {
|
|
129
|
+
console.error("\x1B[31mError: Not logged in.\x1B[0m Run \x1B[33mnobalmako login\x1B[0m or set \x1B[33mNOBALMAKO_TOKEN\x1B[0m.");
|
|
130
|
+
process.exit(1);
|
|
131
|
+
}
|
|
132
|
+
if (!project || !env) {
|
|
133
|
+
console.error("\x1B[31mError: Project and Environment are required.\x1B[0m Use \x1B[33mnobalmako init\x1B[0m or provide flags.");
|
|
134
|
+
process.exit(1);
|
|
135
|
+
}
|
|
136
|
+
console.log(`\x1B[34m[Nobalmako]\x1B[0m Fetching secrets for project \x1B[36m${project}\x1B[0m (\x1B[35m${env}\x1B[0m)...`);
|
|
137
|
+
try {
|
|
138
|
+
const response = await fetch(`${options.apiUrl}/variables?team=${encodeURIComponent(project)}&environment=${encodeURIComponent(env)}`, {
|
|
139
|
+
headers: {
|
|
140
|
+
"Authorization": `Bearer ${token}`
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
if (!response.ok) {
|
|
144
|
+
const errorData = await response.json();
|
|
145
|
+
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
|
146
|
+
}
|
|
147
|
+
const data = await response.json();
|
|
148
|
+
const variables = data.variables || [];
|
|
149
|
+
if (variables.length === 0) {
|
|
150
|
+
console.log("\x1B[33mNo variables found for this project and environment.\x1B[0m");
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
let envContent = `# Generated by Nobalmako on ${(/* @__PURE__ */ new Date()).toISOString()}
|
|
154
|
+
`;
|
|
155
|
+
variables.forEach((v) => {
|
|
156
|
+
if (v.description) {
|
|
157
|
+
envContent += `# ${v.description}
|
|
158
|
+
`;
|
|
159
|
+
}
|
|
160
|
+
envContent += `${v.key}=${v.value}
|
|
161
|
+
`;
|
|
162
|
+
});
|
|
163
|
+
const outputPath = import_path.default.resolve(process.cwd(), file);
|
|
164
|
+
import_fs.default.writeFileSync(outputPath, envContent);
|
|
165
|
+
console.log(`\x1B[32mSuccess!\x1B[0m Pulled ${variables.length} variables into \x1B[33m${file}\x1B[0m`);
|
|
166
|
+
} catch (error) {
|
|
167
|
+
console.error(`\x1B[31mPull failed:\x1B[0m ${error.message}`);
|
|
168
|
+
process.exit(1);
|
|
169
|
+
}
|
|
170
|
+
});
|
|
171
|
+
program.command("push").description("Push environment variables from a .env file to Nobalmako").option("-p, --project <project>", "Project name").option("-e, --env <environment>", "Environment name (e.g. production, staging)").option("-f, --file <filename>", "Input filename").option("-s, --secret", "Mark all variables as secret", false).option("--api-url <url>", "Base API URL", "http://localhost:3000/api").option("-t, --token <token>", "API Token (overrides login)").action(async (options) => {
|
|
172
|
+
const token = getToken(options.token);
|
|
173
|
+
const pConfig = loadProjectConfig();
|
|
174
|
+
const project = options.project || pConfig.project;
|
|
175
|
+
const env = options.env || pConfig.environment;
|
|
176
|
+
const file = options.file || pConfig.file || ".env";
|
|
177
|
+
if (!token) {
|
|
178
|
+
console.error("\x1B[31mError: Not logged in.\x1B[0m Run \x1B[33mnobalmako login\x1B[0m or set \x1B[33mNOBALMAKO_TOKEN\x1B[0m.");
|
|
179
|
+
process.exit(1);
|
|
180
|
+
}
|
|
181
|
+
if (!project || !env) {
|
|
182
|
+
console.error("\x1B[31mError: Project and Environment are required.\x1B[0m Use \x1B[33mnobalmako init\x1B[0m or provide flags.");
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
const inputPath = import_path.default.resolve(process.cwd(), file);
|
|
186
|
+
if (!import_fs.default.existsSync(inputPath)) {
|
|
187
|
+
console.error(`\x1B[31mError: File not found: ${file}\x1B[0m`);
|
|
188
|
+
process.exit(1);
|
|
189
|
+
}
|
|
190
|
+
const envContent = import_fs.default.readFileSync(inputPath, "utf-8");
|
|
191
|
+
const envVars = import_dotenv.default.parse(envContent);
|
|
192
|
+
const keys = Object.keys(envVars);
|
|
193
|
+
console.log(`\x1B[34m[Nobalmako]\x1B[0m Pushing ${keys.length} variables to \x1B[36m${project}\x1B[0m (\x1B[35m${env}\x1B[0m)...`);
|
|
194
|
+
try {
|
|
195
|
+
const envsResponse = await fetch(`${options.apiUrl}/environments`, {
|
|
196
|
+
headers: { "Authorization": `Bearer ${token}` }
|
|
197
|
+
});
|
|
198
|
+
if (!envsResponse.ok) throw new Error("Failed to fetch environments");
|
|
199
|
+
const envsData = await envsResponse.json();
|
|
200
|
+
const targetEnv = envsData.environments.find(
|
|
201
|
+
(e) => e.teamName === project && e.name === env
|
|
202
|
+
);
|
|
203
|
+
if (!targetEnv) {
|
|
204
|
+
throw new Error(`Could not find environment "${env}" for project "${project}".`);
|
|
205
|
+
}
|
|
206
|
+
let successCount = 0;
|
|
207
|
+
for (const [key, value] of Object.entries(envVars)) {
|
|
208
|
+
const response = await fetch(`${options.apiUrl}/variables`, {
|
|
209
|
+
method: "POST",
|
|
210
|
+
headers: {
|
|
211
|
+
"Authorization": `Bearer ${token}`,
|
|
212
|
+
"Content-Type": "application/json"
|
|
213
|
+
},
|
|
214
|
+
body: JSON.stringify({
|
|
215
|
+
key,
|
|
216
|
+
value,
|
|
217
|
+
teamId: targetEnv.teamId,
|
|
218
|
+
environmentId: targetEnv.id,
|
|
219
|
+
isSecret: options.secret
|
|
220
|
+
})
|
|
221
|
+
});
|
|
222
|
+
if (response.ok) {
|
|
223
|
+
successCount++;
|
|
224
|
+
process.stdout.write(`\rProgress: ${successCount}/${keys.length}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
console.log(`
|
|
228
|
+
\x1B[32mSuccess!\x1B[0m Pushed ${successCount} variables to \x1B[33m${project}\x1B[0m`);
|
|
229
|
+
} catch (error) {
|
|
230
|
+
console.error(`\x1B[31mPush failed:\x1B[0m ${error.message}`);
|
|
231
|
+
process.exit(1);
|
|
232
|
+
}
|
|
233
|
+
});
|
|
234
|
+
program.command("run").description("Run a command with environment variables injected").argument("<command...>", "The command to execute").option("-p, --project <project>", "Project name").option("-e, --env <environment>", "Environment name").option("--api-url <url>", "Base API URL", "http://localhost:3000/api").option("-t, --token <token>", "API Token").action(async (commandArgs, options) => {
|
|
235
|
+
const token = getToken(options.token);
|
|
236
|
+
const pConfig = loadProjectConfig();
|
|
237
|
+
const project = options.project || pConfig.project;
|
|
238
|
+
const env = options.env || pConfig.environment;
|
|
239
|
+
if (!token) {
|
|
240
|
+
console.error("\x1B[31mError: Not logged in.\x1B[0m");
|
|
241
|
+
process.exit(1);
|
|
242
|
+
}
|
|
243
|
+
if (!project || !env) {
|
|
244
|
+
console.error("\x1B[31mError: Project and Environment are required.\x1B[0m");
|
|
245
|
+
process.exit(1);
|
|
246
|
+
}
|
|
247
|
+
try {
|
|
248
|
+
const response = await fetch(`${options.apiUrl}/variables?team=${encodeURIComponent(project)}&environment=${encodeURIComponent(env)}`, {
|
|
249
|
+
headers: { "Authorization": `Bearer ${token}` }
|
|
250
|
+
});
|
|
251
|
+
if (!response.ok) throw new Error("Failed to fetch variables");
|
|
252
|
+
const data = await response.json();
|
|
253
|
+
const variables = data.variables || [];
|
|
254
|
+
const newEnv = { ...process.env };
|
|
255
|
+
variables.forEach((v) => {
|
|
256
|
+
newEnv[v.key] = v.value;
|
|
257
|
+
});
|
|
258
|
+
const [cmd, ...args] = commandArgs;
|
|
259
|
+
const child = (0, import_child_process.spawn)(cmd, args, {
|
|
260
|
+
stdio: "inherit",
|
|
261
|
+
env: newEnv,
|
|
262
|
+
shell: true
|
|
263
|
+
});
|
|
264
|
+
child.on("exit", (code) => {
|
|
265
|
+
process.exit(code || 0);
|
|
266
|
+
});
|
|
267
|
+
} catch (error) {
|
|
268
|
+
console.error(`\x1B[31mRun failed:\x1B[0m ${error.message}`);
|
|
269
|
+
process.exit(1);
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
CREATE TABLE "api_keys" (
|
|
2
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
3
|
+
"user_id" uuid NOT NULL,
|
|
4
|
+
"team_id" uuid,
|
|
5
|
+
"name" text NOT NULL,
|
|
6
|
+
"key_hash" text NOT NULL,
|
|
7
|
+
"permissions" jsonb DEFAULT '["read"]'::jsonb,
|
|
8
|
+
"last_used" timestamp,
|
|
9
|
+
"expires_at" timestamp,
|
|
10
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
11
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
12
|
+
);
|
|
13
|
+
--> statement-breakpoint
|
|
14
|
+
CREATE TABLE "audit_logs" (
|
|
15
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
16
|
+
"user_id" uuid,
|
|
17
|
+
"team_id" uuid,
|
|
18
|
+
"action" text NOT NULL,
|
|
19
|
+
"resource_type" text NOT NULL,
|
|
20
|
+
"resource_id" uuid NOT NULL,
|
|
21
|
+
"old_value" jsonb,
|
|
22
|
+
"new_value" jsonb,
|
|
23
|
+
"ip_address" text,
|
|
24
|
+
"user_agent" text,
|
|
25
|
+
"created_at" timestamp DEFAULT now() NOT NULL
|
|
26
|
+
);
|
|
27
|
+
--> statement-breakpoint
|
|
28
|
+
CREATE TABLE "environment_variables" (
|
|
29
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
30
|
+
"team_id" uuid NOT NULL,
|
|
31
|
+
"environment_id" uuid NOT NULL,
|
|
32
|
+
"key" text NOT NULL,
|
|
33
|
+
"value" text NOT NULL,
|
|
34
|
+
"description" text,
|
|
35
|
+
"is_secret" boolean DEFAULT false,
|
|
36
|
+
"tags" jsonb,
|
|
37
|
+
"created_by" uuid NOT NULL,
|
|
38
|
+
"updated_by" uuid,
|
|
39
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
40
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
41
|
+
);
|
|
42
|
+
--> statement-breakpoint
|
|
43
|
+
CREATE TABLE "environments" (
|
|
44
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
45
|
+
"team_id" uuid NOT NULL,
|
|
46
|
+
"name" text NOT NULL,
|
|
47
|
+
"description" text,
|
|
48
|
+
"color" text DEFAULT '#3b82f6',
|
|
49
|
+
"is_default" boolean DEFAULT false,
|
|
50
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
51
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
52
|
+
);
|
|
53
|
+
--> statement-breakpoint
|
|
54
|
+
CREATE TABLE "team_members" (
|
|
55
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
56
|
+
"team_id" uuid NOT NULL,
|
|
57
|
+
"user_id" uuid NOT NULL,
|
|
58
|
+
"role" text DEFAULT 'developer' NOT NULL,
|
|
59
|
+
"joined_at" timestamp DEFAULT now() NOT NULL
|
|
60
|
+
);
|
|
61
|
+
--> statement-breakpoint
|
|
62
|
+
CREATE TABLE "teams" (
|
|
63
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
64
|
+
"name" text NOT NULL,
|
|
65
|
+
"description" text,
|
|
66
|
+
"color" text DEFAULT '#3b82f6',
|
|
67
|
+
"owner_id" uuid NOT NULL,
|
|
68
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
69
|
+
"updated_at" timestamp DEFAULT now() NOT NULL
|
|
70
|
+
);
|
|
71
|
+
--> statement-breakpoint
|
|
72
|
+
CREATE TABLE "users" (
|
|
73
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
74
|
+
"email" text NOT NULL,
|
|
75
|
+
"full_name" text NOT NULL,
|
|
76
|
+
"password" text NOT NULL,
|
|
77
|
+
"avatar" text,
|
|
78
|
+
"role" text DEFAULT 'user' NOT NULL,
|
|
79
|
+
"created_at" timestamp DEFAULT now() NOT NULL,
|
|
80
|
+
"updated_at" timestamp DEFAULT now() NOT NULL,
|
|
81
|
+
CONSTRAINT "users_email_unique" UNIQUE("email")
|
|
82
|
+
);
|
|
83
|
+
--> statement-breakpoint
|
|
84
|
+
CREATE TABLE "variable_history" (
|
|
85
|
+
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
|
|
86
|
+
"variable_id" uuid NOT NULL,
|
|
87
|
+
"team_id" uuid NOT NULL,
|
|
88
|
+
"environment_id" uuid NOT NULL,
|
|
89
|
+
"key" text NOT NULL,
|
|
90
|
+
"value" text NOT NULL,
|
|
91
|
+
"description" text,
|
|
92
|
+
"is_secret" boolean DEFAULT false,
|
|
93
|
+
"changed_by" uuid NOT NULL,
|
|
94
|
+
"change_type" text NOT NULL,
|
|
95
|
+
"created_at" timestamp DEFAULT now() NOT NULL
|
|
96
|
+
);
|
|
97
|
+
--> statement-breakpoint
|
|
98
|
+
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
99
|
+
ALTER TABLE "api_keys" ADD CONSTRAINT "api_keys_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
100
|
+
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE set null ON UPDATE no action;--> statement-breakpoint
|
|
101
|
+
ALTER TABLE "audit_logs" ADD CONSTRAINT "audit_logs_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
102
|
+
ALTER TABLE "environment_variables" ADD CONSTRAINT "environment_variables_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
103
|
+
ALTER TABLE "environment_variables" ADD CONSTRAINT "environment_variables_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
104
|
+
ALTER TABLE "environment_variables" ADD CONSTRAINT "environment_variables_created_by_users_id_fk" FOREIGN KEY ("created_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
105
|
+
ALTER TABLE "environment_variables" ADD CONSTRAINT "environment_variables_updated_by_users_id_fk" FOREIGN KEY ("updated_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
106
|
+
ALTER TABLE "environments" ADD CONSTRAINT "environments_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
107
|
+
ALTER TABLE "team_members" ADD CONSTRAINT "team_members_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
108
|
+
ALTER TABLE "team_members" ADD CONSTRAINT "team_members_user_id_users_id_fk" FOREIGN KEY ("user_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
109
|
+
ALTER TABLE "teams" ADD CONSTRAINT "teams_owner_id_users_id_fk" FOREIGN KEY ("owner_id") REFERENCES "public"."users"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
110
|
+
ALTER TABLE "variable_history" ADD CONSTRAINT "variable_history_variable_id_environment_variables_id_fk" FOREIGN KEY ("variable_id") REFERENCES "public"."environment_variables"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
111
|
+
ALTER TABLE "variable_history" ADD CONSTRAINT "variable_history_team_id_teams_id_fk" FOREIGN KEY ("team_id") REFERENCES "public"."teams"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
112
|
+
ALTER TABLE "variable_history" ADD CONSTRAINT "variable_history_environment_id_environments_id_fk" FOREIGN KEY ("environment_id") REFERENCES "public"."environments"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
|
113
|
+
ALTER TABLE "variable_history" ADD CONSTRAINT "variable_history_changed_by_users_id_fk" FOREIGN KEY ("changed_by") REFERENCES "public"."users"("id") ON DELETE no action ON UPDATE no action;--> statement-breakpoint
|
|
114
|
+
CREATE INDEX "api_keys_user_idx" ON "api_keys" USING btree ("user_id");--> statement-breakpoint
|
|
115
|
+
CREATE INDEX "api_keys_team_idx" ON "api_keys" USING btree ("team_id");--> statement-breakpoint
|
|
116
|
+
CREATE INDEX "audit_logs_user_idx" ON "audit_logs" USING btree ("user_id");--> statement-breakpoint
|
|
117
|
+
CREATE INDEX "audit_logs_team_idx" ON "audit_logs" USING btree ("team_id");--> statement-breakpoint
|
|
118
|
+
CREATE INDEX "audit_logs_resource_idx" ON "audit_logs" USING btree ("resource_type","resource_id");--> statement-breakpoint
|
|
119
|
+
CREATE INDEX "env_vars_team_env_idx" ON "environment_variables" USING btree ("team_id","environment_id");--> statement-breakpoint
|
|
120
|
+
CREATE INDEX "env_vars_key_idx" ON "environment_variables" USING btree ("key");--> statement-breakpoint
|
|
121
|
+
CREATE INDEX "env_vars_created_by_idx" ON "environment_variables" USING btree ("created_by");--> statement-breakpoint
|
|
122
|
+
CREATE INDEX "environments_team_idx" ON "environments" USING btree ("team_id");--> statement-breakpoint
|
|
123
|
+
CREATE INDEX "team_members_team_user_idx" ON "team_members" USING btree ("team_id","user_id");--> statement-breakpoint
|
|
124
|
+
CREATE INDEX "teams_owner_idx" ON "teams" USING btree ("owner_id");--> statement-breakpoint
|
|
125
|
+
CREATE INDEX "variable_history_variable_idx" ON "variable_history" USING btree ("variable_id");--> statement-breakpoint
|
|
126
|
+
CREATE INDEX "variable_history_team_idx" ON "variable_history" USING btree ("team_id");
|