spfn 0.3.0-beta.1 → 0.3.0-beta.2
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 +12 -2
- package/dist/index.js +119 -38
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -411,8 +411,14 @@ listSignups GET /_ops/signups
|
|
|
411
411
|
Add `--json` for the raw JSON Schema. The server still validates every call — `--describe`
|
|
412
412
|
reports what it will accept, and the app's answer decides.
|
|
413
413
|
|
|
414
|
-
The app URL comes from `--app` or `SPFN_OPS_APP
|
|
415
|
-
|
|
414
|
+
The app URL comes from `--app` or `SPFN_OPS_APP`, and it must be **https** — every one of
|
|
415
|
+
these commands carries a secret, and `token issue` carries an administrator's password.
|
|
416
|
+
`http` is accepted only against `localhost`, `127.0.0.1` and `::1`, where there is no
|
|
417
|
+
network to listen on. A URL with a base path (`https://example.com/api`) is kept whole:
|
|
418
|
+
both the ops calls and the administrator sign-in go through it.
|
|
419
|
+
|
|
420
|
+
The ops token resolves `--token` → `SPFN_OPS_TOKEN` → macOS keychain, and its lifecycle is
|
|
421
|
+
managed with:
|
|
416
422
|
|
|
417
423
|
```bash
|
|
418
424
|
spfn ops token issue --name laptop --scopes 'waitlist:read' --app <url>
|
|
@@ -442,6 +448,10 @@ point, which that release added. The CLI does not depend on the package in any f
|
|
|
442
448
|
loads it from the app at run time, and tells a missing package apart from one too old to
|
|
443
449
|
carry the entry point, so the message names the thing to do.
|
|
444
450
|
|
|
451
|
+
Because the package is the app's, it is resolved **from the directory the command runs in**.
|
|
452
|
+
Run `spfn ops token` from the app's root; running it elsewhere reports the package as
|
|
453
|
+
missing, and the message names the directory it looked in.
|
|
454
|
+
|
|
445
455
|
---
|
|
446
456
|
|
|
447
457
|
## Scaffold structure
|
package/dist/index.js
CHANGED
|
@@ -922,7 +922,7 @@ var init_deployment_config = __esm({
|
|
|
922
922
|
|
|
923
923
|
// src/utils/version.ts
|
|
924
924
|
function getCliVersion() {
|
|
925
|
-
return "0.3.0-beta.
|
|
925
|
+
return "0.3.0-beta.2";
|
|
926
926
|
}
|
|
927
927
|
function getTagFromVersion(version) {
|
|
928
928
|
const match = version.match(/-([a-z]+)\./i);
|
|
@@ -5380,7 +5380,39 @@ async function fetchOpsManifest(appUrl, token) {
|
|
|
5380
5380
|
if (manifest?.manifestVersion !== 1 || !Array.isArray(manifest.commands)) {
|
|
5381
5381
|
throw new Error("The manifest answer has an unknown shape.");
|
|
5382
5382
|
}
|
|
5383
|
-
return manifest;
|
|
5383
|
+
return { manifestVersion: 1, commands: usableCommands(manifest.commands) };
|
|
5384
|
+
}
|
|
5385
|
+
var OPS_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
5386
|
+
var OPS_PATH_PREFIX = "/_ops/";
|
|
5387
|
+
function unusableBecause(command) {
|
|
5388
|
+
if (typeof command?.name !== "string" || command.name.length === 0) {
|
|
5389
|
+
return "it has no name";
|
|
5390
|
+
}
|
|
5391
|
+
if (typeof command.method !== "string" || !OPS_METHODS.has(command.method)) {
|
|
5392
|
+
return `its method is ${JSON.stringify(command.method)}`;
|
|
5393
|
+
}
|
|
5394
|
+
if (typeof command.path !== "string" || !command.path.startsWith(OPS_PATH_PREFIX)) {
|
|
5395
|
+
return `its path ${JSON.stringify(command.path)} is outside ${OPS_PATH_PREFIX}`;
|
|
5396
|
+
}
|
|
5397
|
+
if (command.path.split("/").includes("..")) {
|
|
5398
|
+
return `its path ${JSON.stringify(command.path)} climbs out of the ops namespace`;
|
|
5399
|
+
}
|
|
5400
|
+
return null;
|
|
5401
|
+
}
|
|
5402
|
+
function usableCommands(commands) {
|
|
5403
|
+
const usable = [];
|
|
5404
|
+
for (const command of commands) {
|
|
5405
|
+
const reason = unusableBecause(command);
|
|
5406
|
+
if (reason !== null) {
|
|
5407
|
+
console.error(`\u26A0\uFE0F Ignoring an ops command the manifest announced: ${reason}.`);
|
|
5408
|
+
continue;
|
|
5409
|
+
}
|
|
5410
|
+
usable.push({ ...command, input: isPlainObject(command.input) ? command.input : {} });
|
|
5411
|
+
}
|
|
5412
|
+
return usable;
|
|
5413
|
+
}
|
|
5414
|
+
function isPlainObject(value) {
|
|
5415
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5384
5416
|
}
|
|
5385
5417
|
function buildCommandPath(command, params, query) {
|
|
5386
5418
|
const path5 = command.path.replace(/:([A-Za-z0-9_]+)/g, (_match, name) => {
|
|
@@ -5407,6 +5439,10 @@ var SECTIONS = [
|
|
|
5407
5439
|
function isSchema(value) {
|
|
5408
5440
|
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
5409
5441
|
}
|
|
5442
|
+
var MAX_SCHEMA_DEPTH = 12;
|
|
5443
|
+
function plain(value) {
|
|
5444
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "?");
|
|
5445
|
+
}
|
|
5410
5446
|
function typeName(schema) {
|
|
5411
5447
|
if (Array.isArray(schema.enum)) {
|
|
5412
5448
|
return schema.enum.every((v) => typeof v === "string") ? "string" : "value";
|
|
@@ -5453,7 +5489,7 @@ function constraintNotes(schema) {
|
|
|
5453
5489
|
}
|
|
5454
5490
|
return notes;
|
|
5455
5491
|
}
|
|
5456
|
-
function collectFields(schema, prefix = "") {
|
|
5492
|
+
function collectFields(schema, prefix = "", depth = 0) {
|
|
5457
5493
|
const properties = isSchema(schema.properties) ? schema.properties : {};
|
|
5458
5494
|
const required = Array.isArray(schema.required) ? schema.required : [];
|
|
5459
5495
|
const rows = [];
|
|
@@ -5461,17 +5497,17 @@ function collectFields(schema, prefix = "") {
|
|
|
5461
5497
|
if (!isSchema(raw)) {
|
|
5462
5498
|
continue;
|
|
5463
5499
|
}
|
|
5464
|
-
const path5 = prefix ? `${prefix}.${name}` : name;
|
|
5465
|
-
const nested = isSchema(raw.properties) ? collectFields(raw, path5) : [];
|
|
5500
|
+
const path5 = prefix ? `${prefix}.${plain(name)}` : plain(name);
|
|
5501
|
+
const nested = isSchema(raw.properties) && depth < MAX_SCHEMA_DEPTH ? collectFields(raw, path5, depth + 1) : [];
|
|
5466
5502
|
if (nested.length > 0) {
|
|
5467
5503
|
rows.push(...nested);
|
|
5468
5504
|
continue;
|
|
5469
5505
|
}
|
|
5470
5506
|
rows.push({
|
|
5471
5507
|
name: path5,
|
|
5472
|
-
type: typeName(raw),
|
|
5508
|
+
type: plain(typeName(raw)),
|
|
5473
5509
|
requirement: required.includes(name) ? "required" : "optional",
|
|
5474
|
-
notes: constraintNotes(raw).join(", ")
|
|
5510
|
+
notes: plain(constraintNotes(raw).join(", "))
|
|
5475
5511
|
});
|
|
5476
5512
|
}
|
|
5477
5513
|
return rows;
|
|
@@ -5490,7 +5526,7 @@ function renderSection(label, flag, rows) {
|
|
|
5490
5526
|
return lines;
|
|
5491
5527
|
}
|
|
5492
5528
|
function renderCommandUsage(command) {
|
|
5493
|
-
const lines = [`${command.name} ${command.method} ${command.path}`, ""];
|
|
5529
|
+
const lines = [`${plain(command.name)} ${command.method} ${plain(command.path)}`, ""];
|
|
5494
5530
|
let described = false;
|
|
5495
5531
|
for (const section of SECTIONS) {
|
|
5496
5532
|
const schema = command.input[section.key];
|
|
@@ -5507,7 +5543,7 @@ function renderCommandUsage(command) {
|
|
|
5507
5543
|
if (!described) {
|
|
5508
5544
|
lines.push(" Takes no input.", "");
|
|
5509
5545
|
}
|
|
5510
|
-
lines.push(` Invoke: spfn ops call ${command.name}${exampleFlags(command)}`);
|
|
5546
|
+
lines.push(` Invoke: spfn ops call ${plain(command.name)}${exampleFlags(command)}`);
|
|
5511
5547
|
return lines.join("\n");
|
|
5512
5548
|
}
|
|
5513
5549
|
function exampleFlags(command) {
|
|
@@ -5598,18 +5634,37 @@ function deleteOpsToken(account) {
|
|
|
5598
5634
|
}
|
|
5599
5635
|
|
|
5600
5636
|
// src/commands/ops/resolve.ts
|
|
5637
|
+
function isLoopback(hostname) {
|
|
5638
|
+
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "[::1]" || hostname.endsWith(".localhost");
|
|
5639
|
+
}
|
|
5640
|
+
function assertTransportSafe(parsed, appUrl) {
|
|
5641
|
+
if (parsed.protocol === "https:" || parsed.protocol === "http:" && isLoopback(parsed.hostname)) {
|
|
5642
|
+
return;
|
|
5643
|
+
}
|
|
5644
|
+
if (parsed.protocol === "http:") {
|
|
5645
|
+
console.error(chalk33.red(`\u274C Refusing to talk to ${appUrl} over http.`));
|
|
5646
|
+
console.error(chalk33.gray(" An administrator password and an ops token cross this connection, and http sends both in the clear."));
|
|
5647
|
+
console.error(chalk33.gray(" Use https. http is allowed only against localhost."));
|
|
5648
|
+
process.exit(1);
|
|
5649
|
+
}
|
|
5650
|
+
console.error(chalk33.red(`\u274C App URL must be https, got "${parsed.protocol}//".`));
|
|
5651
|
+
console.error(chalk33.gray(" The ops surface is reached over HTTP(S) \u2014 http is allowed only against localhost."));
|
|
5652
|
+
process.exit(1);
|
|
5653
|
+
}
|
|
5601
5654
|
function resolveAppUrl(options) {
|
|
5602
5655
|
const appUrl = options.app ?? process.env.SPFN_OPS_APP;
|
|
5603
5656
|
if (!appUrl) {
|
|
5604
5657
|
console.error(chalk33.red("\u274C No app URL. Pass --app <url> or set SPFN_OPS_APP."));
|
|
5605
5658
|
process.exit(1);
|
|
5606
5659
|
}
|
|
5660
|
+
let parsed;
|
|
5607
5661
|
try {
|
|
5608
|
-
new URL(appUrl);
|
|
5662
|
+
parsed = new URL(appUrl);
|
|
5609
5663
|
} catch {
|
|
5610
5664
|
console.error(chalk33.red(`\u274C Invalid app URL: ${appUrl}`));
|
|
5611
5665
|
process.exit(1);
|
|
5612
5666
|
}
|
|
5667
|
+
assertTransportSafe(parsed, appUrl);
|
|
5613
5668
|
return appUrl;
|
|
5614
5669
|
}
|
|
5615
5670
|
function appAccount(appUrl) {
|
|
@@ -5623,7 +5678,13 @@ async function resolveToken(options, appUrl) {
|
|
|
5623
5678
|
return process.env.SPFN_OPS_TOKEN;
|
|
5624
5679
|
}
|
|
5625
5680
|
if (keychainSupported()) {
|
|
5626
|
-
const stored = await loadOpsToken(appAccount(appUrl))
|
|
5681
|
+
const stored = await loadOpsToken(appAccount(appUrl)).catch((err) => {
|
|
5682
|
+
console.error(chalk33.yellow(
|
|
5683
|
+
`\u26A0\uFE0F Could not read the keychain (${err instanceof Error ? err.message : String(err)}).`
|
|
5684
|
+
));
|
|
5685
|
+
console.error(chalk33.gray(" Unlock it, or pass --token / set SPFN_OPS_TOKEN for this run."));
|
|
5686
|
+
return null;
|
|
5687
|
+
});
|
|
5627
5688
|
if (stored) {
|
|
5628
5689
|
return stored;
|
|
5629
5690
|
}
|
|
@@ -5654,16 +5715,26 @@ import prompts9 from "prompts";
|
|
|
5654
5715
|
import chalk34 from "chalk";
|
|
5655
5716
|
|
|
5656
5717
|
// src/utils/ops/auth-crypto.ts
|
|
5718
|
+
import { createRequire } from "module";
|
|
5719
|
+
import { join as join25 } from "path";
|
|
5720
|
+
import { pathToFileURL as pathToFileURL3 } from "url";
|
|
5657
5721
|
var CRYPTO_ENTRY_SINCE = "0.3.0-beta.2";
|
|
5658
5722
|
var CRYPTO_ENTRY = "@spfn/auth/crypto";
|
|
5723
|
+
async function importFromApp(specifier) {
|
|
5724
|
+
const requireFromApp = createRequire(join25(process.cwd(), "noop.js"));
|
|
5725
|
+
return await import(pathToFileURL3(requireFromApp.resolve(specifier)).href);
|
|
5726
|
+
}
|
|
5659
5727
|
async function loadAuthCrypto() {
|
|
5660
5728
|
try {
|
|
5661
|
-
return await
|
|
5729
|
+
return await importFromApp(CRYPTO_ENTRY);
|
|
5662
5730
|
} catch (err) {
|
|
5663
5731
|
const code = err.code;
|
|
5664
5732
|
if (code === "ERR_MODULE_NOT_FOUND" || code === "MODULE_NOT_FOUND") {
|
|
5665
5733
|
throw new Error(
|
|
5666
|
-
|
|
5734
|
+
`No @spfn/auth is installed in ${process.cwd()}, and ops tokens live in its schema \u2014 so this app has none to issue, list or revoke.
|
|
5735
|
+
An app that uses the ops surface installs @spfn/auth for opsTokenAuth; add it there.
|
|
5736
|
+
Run this from the app directory \u2014 the package is resolved from where the command runs.
|
|
5737
|
+
Invoking commands with a token you already hold (spfn ops list / call) does not need it.`
|
|
5667
5738
|
);
|
|
5668
5739
|
}
|
|
5669
5740
|
if (code === "ERR_PACKAGE_PATH_NOT_EXPORTED") {
|
|
@@ -5677,8 +5748,18 @@ async function loadAuthCrypto() {
|
|
|
5677
5748
|
}
|
|
5678
5749
|
|
|
5679
5750
|
// src/utils/ops/admin-session.ts
|
|
5751
|
+
function parseJsonAnswer(text, status, statusText) {
|
|
5752
|
+
if (!text) {
|
|
5753
|
+
return {};
|
|
5754
|
+
}
|
|
5755
|
+
try {
|
|
5756
|
+
return JSON.parse(text);
|
|
5757
|
+
} catch {
|
|
5758
|
+
throw new Error(`The app answered ${status} ${statusText} with something that is not JSON.`);
|
|
5759
|
+
}
|
|
5760
|
+
}
|
|
5680
5761
|
async function postJson(appUrl, path5, body, authorization) {
|
|
5681
|
-
const response = await fetch(
|
|
5762
|
+
const response = await fetch(joinUrl(appUrl, path5), {
|
|
5682
5763
|
method: "POST",
|
|
5683
5764
|
headers: {
|
|
5684
5765
|
"Content-Type": "application/json",
|
|
@@ -5687,7 +5768,7 @@ async function postJson(appUrl, path5, body, authorization) {
|
|
|
5687
5768
|
body: JSON.stringify(body)
|
|
5688
5769
|
});
|
|
5689
5770
|
const text = await response.text();
|
|
5690
|
-
const parsed = text
|
|
5771
|
+
const parsed = parseJsonAnswer(text, response.status, response.statusText);
|
|
5691
5772
|
if (!response.ok) {
|
|
5692
5773
|
throw new Error(parsed.message ?? parsed.error ?? `${response.status} ${response.statusText}`);
|
|
5693
5774
|
}
|
|
@@ -5752,7 +5833,7 @@ async function withAdminSession(appUrl, run2) {
|
|
|
5752
5833
|
return result;
|
|
5753
5834
|
}
|
|
5754
5835
|
async function adminRequest(appUrl, method, path5, session, body) {
|
|
5755
|
-
const response = await fetch(
|
|
5836
|
+
const response = await fetch(joinUrl(appUrl, path5), {
|
|
5756
5837
|
method,
|
|
5757
5838
|
headers: {
|
|
5758
5839
|
Authorization: session.authorization,
|
|
@@ -5761,7 +5842,7 @@ async function adminRequest(appUrl, method, path5, session, body) {
|
|
|
5761
5842
|
...body === void 0 ? {} : { body: JSON.stringify(body) }
|
|
5762
5843
|
});
|
|
5763
5844
|
const text = await response.text();
|
|
5764
|
-
const parsed = text
|
|
5845
|
+
const parsed = parseJsonAnswer(text, response.status, response.statusText);
|
|
5765
5846
|
if (!response.ok) {
|
|
5766
5847
|
throw new Error(parsed.message ?? parsed.error ?? `${response.status} ${response.statusText}`);
|
|
5767
5848
|
}
|
|
@@ -5920,7 +6001,7 @@ async function listCommands(options) {
|
|
|
5920
6001
|
console.log(chalk36.bold(`Ops commands at ${appUrl}:
|
|
5921
6002
|
`));
|
|
5922
6003
|
for (const command of manifest.commands) {
|
|
5923
|
-
console.log(` ${chalk36.cyan(command.name)} ${chalk36.gray(`${command.method} ${command.path}`)} input: ${inputSummary(command)}`);
|
|
6004
|
+
console.log(` ${chalk36.cyan(plain(command.name))} ${chalk36.gray(`${command.method} ${plain(command.path)}`)} input: ${inputSummary(command)}`);
|
|
5924
6005
|
}
|
|
5925
6006
|
console.log(chalk36.gray(`
|
|
5926
6007
|
\u{1F4A1} Invoke: spfn ops call <name> [--param k=v] [--query k=v] [--data '{"..."}']`));
|
|
@@ -5932,8 +6013,8 @@ async function callCommand(name, options) {
|
|
|
5932
6013
|
const manifest = await fetchOpsManifest(appUrl, token);
|
|
5933
6014
|
const command = manifest.commands.find((c) => c.name === name);
|
|
5934
6015
|
if (!command) {
|
|
5935
|
-
console.error(chalk36.red(`\u274C Unknown ops command "${name}".`));
|
|
5936
|
-
console.error(chalk36.gray(` Known: ${manifest.commands.map((c) => c.name).join(", ") || "(none)"}`));
|
|
6016
|
+
console.error(chalk36.red(`\u274C Unknown ops command "${plain(name)}".`));
|
|
6017
|
+
console.error(chalk36.gray(` Known: ${manifest.commands.map((c) => plain(c.name)).join(", ") || "(none)"}`));
|
|
5937
6018
|
process.exit(1);
|
|
5938
6019
|
}
|
|
5939
6020
|
if (options.describe) {
|
|
@@ -5943,7 +6024,7 @@ async function callCommand(name, options) {
|
|
|
5943
6024
|
let body;
|
|
5944
6025
|
if (options.data !== void 0) {
|
|
5945
6026
|
if (command.method === "GET") {
|
|
5946
|
-
console.error(chalk36.red(`\u274C "${name}" is a ${command.method} command and takes no request body.`));
|
|
6027
|
+
console.error(chalk36.red(`\u274C "${plain(name)}" is a ${command.method} command and takes no request body.`));
|
|
5947
6028
|
console.error(chalk36.gray(" Pass its input with --query k=v (or --param k=v for path segments)."));
|
|
5948
6029
|
process.exit(1);
|
|
5949
6030
|
}
|
|
@@ -5964,7 +6045,7 @@ async function callCommand(name, options) {
|
|
|
5964
6045
|
console.log(rendered);
|
|
5965
6046
|
return;
|
|
5966
6047
|
}
|
|
5967
|
-
console.error(chalk36.red(`\u274C ${command.method} ${command.path} answered ${response.status}`));
|
|
6048
|
+
console.error(chalk36.red(`\u274C ${command.method} ${plain(command.path)} answered ${response.status}`));
|
|
5968
6049
|
console.error(rendered);
|
|
5969
6050
|
process.exit(1);
|
|
5970
6051
|
}
|
|
@@ -5994,7 +6075,7 @@ function resolveEnv(env9) {
|
|
|
5994
6075
|
|
|
5995
6076
|
// src/commands/secret/store-value.ts
|
|
5996
6077
|
init_logger();
|
|
5997
|
-
import { join as
|
|
6078
|
+
import { join as join27 } from "path";
|
|
5998
6079
|
init_env_file();
|
|
5999
6080
|
|
|
6000
6081
|
// src/utils/sops.ts
|
|
@@ -6043,19 +6124,19 @@ async function sopsUpdateKeys(absFile) {
|
|
|
6043
6124
|
|
|
6044
6125
|
// src/utils/secret-config.ts
|
|
6045
6126
|
import { existsSync as existsSync28, readdirSync as readdirSync2 } from "fs";
|
|
6046
|
-
import { join as
|
|
6127
|
+
import { join as join26 } from "path";
|
|
6047
6128
|
var SECRETS_DIR = "secrets";
|
|
6048
6129
|
function getSopsFile(cwd, env9) {
|
|
6049
6130
|
const relFile = `${SECRETS_DIR}/${env9}.enc.json`;
|
|
6050
|
-
return { absFile:
|
|
6131
|
+
return { absFile: join26(cwd, relFile), relFile };
|
|
6051
6132
|
}
|
|
6052
6133
|
function findUp(cwd, filename, maxDepth = 6) {
|
|
6053
6134
|
let dir = cwd;
|
|
6054
6135
|
for (let depth = 0; depth < maxDepth; depth++) {
|
|
6055
|
-
if (existsSync28(
|
|
6136
|
+
if (existsSync28(join26(dir, filename))) {
|
|
6056
6137
|
return dir;
|
|
6057
6138
|
}
|
|
6058
|
-
const parent =
|
|
6139
|
+
const parent = join26(dir, "..");
|
|
6059
6140
|
if (parent === dir) {
|
|
6060
6141
|
break;
|
|
6061
6142
|
}
|
|
@@ -6065,17 +6146,17 @@ function findUp(cwd, filename, maxDepth = 6) {
|
|
|
6065
6146
|
}
|
|
6066
6147
|
function findSopsConfig(cwd) {
|
|
6067
6148
|
const dir = findUp(cwd, ".sops.yaml");
|
|
6068
|
-
return dir ?
|
|
6149
|
+
return dir ? join26(dir, ".sops.yaml") : null;
|
|
6069
6150
|
}
|
|
6070
6151
|
function hasSopsConfig(cwd) {
|
|
6071
6152
|
return findSopsConfig(cwd) !== null;
|
|
6072
6153
|
}
|
|
6073
6154
|
function listSopsFiles(cwd) {
|
|
6074
|
-
const dir =
|
|
6155
|
+
const dir = join26(cwd, SECRETS_DIR);
|
|
6075
6156
|
if (!existsSync28(dir)) {
|
|
6076
6157
|
return [];
|
|
6077
6158
|
}
|
|
6078
|
-
return readdirSync2(dir).filter((name) => name.endsWith(".enc.json")).map((name) =>
|
|
6159
|
+
return readdirSync2(dir).filter((name) => name.endsWith(".enc.json")).map((name) => join26(dir, name));
|
|
6079
6160
|
}
|
|
6080
6161
|
|
|
6081
6162
|
// src/commands/secret/store-value.ts
|
|
@@ -6104,7 +6185,7 @@ async function storeSecret(cwd, env9, key, value) {
|
|
|
6104
6185
|
);
|
|
6105
6186
|
}
|
|
6106
6187
|
await store.set(keychainName(key), value);
|
|
6107
|
-
const serverEnvPath =
|
|
6188
|
+
const serverEnvPath = join27(cwd, ".env.server");
|
|
6108
6189
|
const result = upsertEnvVar(serverEnvPath, key, keychainRef(key));
|
|
6109
6190
|
restrictEnvFilePerms(serverEnvPath);
|
|
6110
6191
|
ensureGitignored(cwd, [{ pattern: ".env.server", comment: "spfn server env (secrets)" }]);
|
|
@@ -6344,10 +6425,10 @@ init_logger();
|
|
|
6344
6425
|
import { execa as execa12 } from "execa";
|
|
6345
6426
|
import { existsSync as existsSync29, mkdirSync as mkdirSync6 } from "fs";
|
|
6346
6427
|
import { homedir } from "os";
|
|
6347
|
-
import { dirname as dirname5, join as
|
|
6428
|
+
import { dirname as dirname5, join as join28 } from "path";
|
|
6348
6429
|
import chalk41 from "chalk";
|
|
6349
6430
|
function ageKeyFile() {
|
|
6350
|
-
return process.env.SOPS_AGE_KEY_FILE ??
|
|
6431
|
+
return process.env.SOPS_AGE_KEY_FILE ?? join28(homedir(), ".config", "sops", "age", "keys.txt");
|
|
6351
6432
|
}
|
|
6352
6433
|
async function ensureAgeInstalled() {
|
|
6353
6434
|
try {
|
|
@@ -6395,7 +6476,7 @@ function printPublicKeys(keys) {
|
|
|
6395
6476
|
// src/commands/secret/recipients.ts
|
|
6396
6477
|
init_logger();
|
|
6397
6478
|
import { existsSync as existsSync30, readFileSync as readFileSync12, writeFileSync as writeFileSync19 } from "fs";
|
|
6398
|
-
import { join as
|
|
6479
|
+
import { join as join29 } from "path";
|
|
6399
6480
|
import { parse as parse3, stringify } from "yaml";
|
|
6400
6481
|
import chalk42 from "chalk";
|
|
6401
6482
|
var SOPS_CONFIG = ".sops.yaml";
|
|
@@ -6403,7 +6484,7 @@ var DEFAULT_PATH_REGEX = "secrets/.*\\.enc\\.json$";
|
|
|
6403
6484
|
var AGE_RECIPIENT = /^age1[0-9a-z]+$/;
|
|
6404
6485
|
async function secretRecipients(action, key, _options) {
|
|
6405
6486
|
const cwd = process.cwd();
|
|
6406
|
-
const configPath =
|
|
6487
|
+
const configPath = join29(cwd, SOPS_CONFIG);
|
|
6407
6488
|
if (action === "list") {
|
|
6408
6489
|
listRecipients(configPath);
|
|
6409
6490
|
return;
|
|
@@ -6488,7 +6569,7 @@ async function reencrypt(cwd) {
|
|
|
6488
6569
|
|
|
6489
6570
|
// src/commands/secret/check.ts
|
|
6490
6571
|
init_logger();
|
|
6491
|
-
import { join as
|
|
6572
|
+
import { join as join30 } from "path";
|
|
6492
6573
|
import chalk43 from "chalk";
|
|
6493
6574
|
init_env_file();
|
|
6494
6575
|
var COMMITTED_FILES = [".env", ".env.example"];
|
|
@@ -6507,14 +6588,14 @@ async function secretCheck(options) {
|
|
|
6507
6588
|
const issues = [];
|
|
6508
6589
|
const warnings = [];
|
|
6509
6590
|
for (const file of COMMITTED_FILES) {
|
|
6510
|
-
const parsed = parseEnvFile(
|
|
6591
|
+
const parsed = parseEnvFile(join30(cwd, file));
|
|
6511
6592
|
for (const [key, value] of Object.entries(parsed)) {
|
|
6512
6593
|
if (secretKeys.has(key) && value.length > 0 && !PLACEHOLDER.test(value)) {
|
|
6513
6594
|
issues.push(`${chalk43.cyan(key)} has a real value in committed ${chalk43.yellow(file)} \u2014 move it to the keychain/SOPS.`);
|
|
6514
6595
|
}
|
|
6515
6596
|
}
|
|
6516
6597
|
}
|
|
6517
|
-
const serverEnv = parseEnvFile(
|
|
6598
|
+
const serverEnv = parseEnvFile(join30(cwd, ".env.server"));
|
|
6518
6599
|
for (const key of secretKeys) {
|
|
6519
6600
|
const value = serverEnv[key];
|
|
6520
6601
|
if (value && !value.startsWith(KEYCHAIN_REF_PREFIX)) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spfn",
|
|
3
|
-
"version": "0.3.0-beta.
|
|
3
|
+
"version": "0.3.0-beta.2",
|
|
4
4
|
"description": "Scaffold a full-stack TypeScript backend onto a Next.js app built with an AI coding agent: auth, database, typed routes and codegen, one fixed vertical slice per feature",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|