New:Socket for Asana Is Now Available.Learn more
Get Started

firth

Package Overview
Dependencies
Maintainers
1
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

firth - npm Package Compare versions

Comparing version
0.0.4
to
0.0.5
+78
dist/commands/govern.js
import { readProjectLink } from '../config.js';
import { apiFromDeps } from './project.js';
// If a gated control-plane action returned `approval_required`, print the guidance and signal "not done".
export function reportIfGated(res, deps) {
if (res && res.status === 'approval_required') {
deps.print(`⛔ ${res.action} requires approval (id ${res.approvalId}) — have a human run \`firth approve ${res.approvalId}\`, then re-run.`);
return true;
}
return false;
}
function linkedProjectId(deps) {
const link = readProjectLink(deps.cwd);
if (!link) {
deps.print('this directory is not linked — run `firth project link <id>`');
return null;
}
return link.projectId;
}
export async function approvals(_argv, deps) {
const projectId = linkedProjectId(deps);
if (!projectId)
return 1;
const list = await apiFromDeps(deps).listApprovals(projectId, 'pending');
if (list.length === 0) {
deps.print('no pending approvals');
return 0;
}
for (const a of list)
deps.print(`${a.id} ${a.action} (requested ${a.requested_at})`);
return 0;
}
export async function approve(argv, deps) {
const id = argv[0];
if (!id) {
deps.print('usage: firth approve <id>');
return 1;
}
const projectId = linkedProjectId(deps);
if (!projectId)
return 1;
await apiFromDeps(deps).approve(projectId, id);
deps.print(`approved ${id}`);
return 0;
}
export async function deny(argv, deps) {
const id = argv[0];
if (!id) {
deps.print('usage: firth deny <id>');
return 1;
}
const projectId = linkedProjectId(deps);
if (!projectId)
return 1;
await apiFromDeps(deps).deny(projectId, id);
deps.print(`denied ${id}`);
return 0;
}
export async function policy(argv, deps) {
const projectId = linkedProjectId(deps);
if (!projectId)
return 1;
const api = apiFromDeps(deps);
if (argv[0] === 'set') {
const [, action, decision] = argv;
if (!action || !decision) {
deps.print('usage: firth policy set <action> <allow|deny|approve>');
return 1;
}
const p = await api.setPolicy(projectId, action, decision);
for (const [a, d] of Object.entries(p))
deps.print(`${a}: ${d}`);
return 0;
}
const p = await api.getPolicy(projectId);
for (const [a, d] of Object.entries(p))
deps.print(`${a}: ${d}`);
return 0;
}
+10
-1

@@ -70,4 +70,13 @@ const realFetcher = async (url, init) => {

const q = branch ? `?branch=${encodeURIComponent(branch)}` : '';
return this.req('GET', `/projects/${projectId}/secrets${q}`).then((r) => r.secrets);
return this.req('GET', `/projects/${projectId}/secrets${q}`);
}
listApprovals(projectId, status) {
return this.req('GET', `/projects/${projectId}/approvals${status ? `?status=${status}` : ''}`).then((r) => r.approvals);
}
approve(projectId, id) { return this.req('POST', `/projects/${projectId}/approvals/${id}/approve`).then((r) => r.approval); }
deny(projectId, id) { return this.req('POST', `/projects/${projectId}/approvals/${id}/deny`).then((r) => r.approval); }
getPolicy(projectId) { return this.req('GET', `/projects/${projectId}/policy`).then((r) => r.policy); }
setPolicy(projectId, action, decision) {
return this.req('PUT', `/projects/${projectId}/policy/${action}`, { decision }).then((r) => r.policy);
}
deploy(projectId, opts) {

@@ -74,0 +83,0 @@ return this.req('POST', `/projects/${projectId}/deploy`, opts);

@@ -5,2 +5,3 @@ import { parseArgs } from 'node:util';

import { formatTeardown } from './util.js';
import { reportIfGated } from './govern.js';
import { ensureFlyctl } from '../fly.js';

@@ -96,2 +97,4 @@ import { ensureSkills } from '../ensure-skills.js';

const out = await apiFromDeps(deps).deleteBranch(projectId, target.id);
if (reportIfGated(out, deps))
return 1;
// If the deleted branch is the current one, clear it

@@ -98,0 +101,0 @@ const link = readProjectLink(deps.cwd);

@@ -8,2 +8,3 @@ import { existsSync } from 'node:fs';

import { apiFromDeps } from './project.js';
import { reportIfGated } from './govern.js';
export async function deploy(argv, deps) {

@@ -34,2 +35,4 @@ const { values, positionals } = parseArgs({ args: argv, options: {

});
if (reportIfGated(out, deps))
return 1;
deps.print(`deployed machine ${out.machineId} → ${out.url}`);

@@ -50,2 +53,4 @@ return 0;

const out = await api.deploy(link.projectId, { image: imageRef, from, branch, port });
if (reportIfGated(out, deps))
return 1;
deps.print(`deployed machine ${out.machineId} → ${out.url}`);

@@ -52,0 +57,0 @@ deps.print(`image: ${imageRef} (built remotely)`);

@@ -7,2 +7,3 @@ import { parseArgs } from 'node:util';

import { ensureSkills } from '../ensure-skills.js';
import { reportIfGated } from './govern.js';
// Build a FirthApi from stored config; tests can override via deps.makeApi.

@@ -80,2 +81,4 @@ export function apiFromDeps(deps) {

const out = await apiFromDeps(deps).deleteProject(link.projectId);
if (reportIfGated(out, deps))
return 1;
clearProjectLink(deps.cwd);

@@ -82,0 +85,0 @@ deps.print(`deleted project ${link.projectId}${formatTeardown(out.teardown ?? {})}; unlinked ./.firth/project.json`);

@@ -6,2 +6,3 @@ import { existsSync, readFileSync, writeFileSync } from 'node:fs';

import { apiFromDeps } from './project.js';
import { reportIfGated } from './govern.js';
export async function secrets(argv, deps) {

@@ -28,4 +29,8 @@ const { values } = parseArgs({ args: argv, options: { branch: { type: 'string' } }, allowPositionals: false });

const project = await api.getSecrets(link.projectId);
if (reportIfGated(project, deps))
return 1;
const branch = await api.getSecrets(link.projectId, target.id);
const bundle = { ...project, ...branch };
if (reportIfGated(branch, deps))
return 1;
const bundle = { ...(project.secrets ?? {}), ...(branch.secrets ?? {}) };
// Merge Firth-managed keys into any existing .env, preserving user-added lines/comments.

@@ -32,0 +37,0 @@ const path = join(deps.cwd, '.env');

@@ -15,2 +15,3 @@ #!/usr/bin/env node

import { status } from './commands/status.js';
import { approvals, approve, deny, policy } from './commands/govern.js';
import { defaultRunner } from './fly.js';

@@ -46,2 +47,6 @@ import { defaultBuildRunner } from './flyctl-build.js';

branch delete <name> Delete a branch + its Neon branch (--yes)
approvals List pending approvals
approve <id> Approve a pending request
deny <id> Deny a pending request
policy [set <a> <d>] Show or set the project's govern policy
--help Show this help

@@ -67,2 +72,6 @@ --version, -v Print the CLI version`;

COMMANDS['status'] = status;
COMMANDS['approvals'] = approvals;
COMMANDS['approve'] = approve;
COMMANDS['deny'] = deny;
COMMANDS['policy'] = policy;
export async function route(argv, deps) {

@@ -69,0 +78,0 @@ if (argv.length === 0 || argv[0] === '--help' || argv[0] === '-h') {

+1
-1
{
"name": "firth",
"version": "0.0.4",
"version": "0.0.5",
"description": "The Firth CLI — provision and govern a project's cloud resources (Neon Postgres, Tigris storage, Fly.io compute) behind one credential seam.",

@@ -5,0 +5,0 @@ "type": "module",