spfn 0.3.0-beta.13 → 0.3.0-beta.15
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 +45 -8
- package/dist/index.js +359 -216
- package/dist/surface.d.ts +228 -0
- package/dist/surface.js +497 -0
- package/package.json +14 -1
package/dist/index.js
CHANGED
|
@@ -949,7 +949,7 @@ import { dirname as dirname2, join as join10 } from "path";
|
|
|
949
949
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
950
950
|
function getCliVersion() {
|
|
951
951
|
if (true) {
|
|
952
|
-
return "0.3.0-beta.
|
|
952
|
+
return "0.3.0-beta.15";
|
|
953
953
|
}
|
|
954
954
|
return fromPackageJson ?? (fromPackageJson = readPackageVersion());
|
|
955
955
|
}
|
|
@@ -5375,8 +5375,8 @@ function secretEntries(schema) {
|
|
|
5375
5375
|
function generatableSecrets(schema) {
|
|
5376
5376
|
return secretEntries(schema).filter((entry) => !!entry.generate);
|
|
5377
5377
|
}
|
|
5378
|
-
function
|
|
5379
|
-
return entry.layer
|
|
5378
|
+
function allowsLayer(entry, layer) {
|
|
5379
|
+
return entry.layer === void 0 || entry.layer === layer;
|
|
5380
5380
|
}
|
|
5381
5381
|
|
|
5382
5382
|
// src/utils/env-list.ts
|
|
@@ -5432,7 +5432,7 @@ function describeEnvList(options) {
|
|
|
5432
5432
|
return options.package ?? "whole app";
|
|
5433
5433
|
}
|
|
5434
5434
|
function describeRules(entry) {
|
|
5435
|
-
return `"${entry.type}" (${entry.required ? "required" : "optional"}${entry.sensitive ? ", sensitive" : ""}${
|
|
5435
|
+
return `"${entry.type}" (${entry.required ? "required" : "optional"}${entry.sensitive ? ", sensitive" : ""}${entry.layer ? `, ${entry.layer} layer` : ""})`;
|
|
5436
5436
|
}
|
|
5437
5437
|
async function loadAppSchemas(cwd) {
|
|
5438
5438
|
const paths = (await loadAppConfig3(cwd)).env?.schemas ?? [];
|
|
@@ -6339,80 +6339,23 @@ envCommand.command("audit").description("Find direct process.env reads and app e
|
|
|
6339
6339
|
import { Command as Command15 } from "commander";
|
|
6340
6340
|
import chalk36 from "chalk";
|
|
6341
6341
|
|
|
6342
|
-
// src/
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
}
|
|
6346
|
-
|
|
6347
|
-
// src/utils/ops/client.ts
|
|
6348
|
-
function joinUrl(appUrl, path5) {
|
|
6349
|
-
return appUrl.replace(/\/+$/, "") + path5;
|
|
6350
|
-
}
|
|
6351
|
-
async function request(appUrl, token, method, path5, body) {
|
|
6352
|
-
const response = await fetch(joinUrl(appUrl, path5), {
|
|
6353
|
-
method,
|
|
6354
|
-
headers: {
|
|
6355
|
-
Authorization: `Bearer ${token}`,
|
|
6356
|
-
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
6357
|
-
},
|
|
6358
|
-
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
6359
|
-
});
|
|
6360
|
-
const text = await response.text();
|
|
6361
|
-
let parsed = text;
|
|
6362
|
-
try {
|
|
6363
|
-
parsed = text.length > 0 ? JSON.parse(text) : null;
|
|
6364
|
-
} catch {
|
|
6365
|
-
}
|
|
6366
|
-
return { status: response.status, body: parsed };
|
|
6367
|
-
}
|
|
6368
|
-
async function fetchOpsManifest(appUrl, token) {
|
|
6369
|
-
const { status, body } = await request(appUrl, token, "GET", "/_ops/_manifest");
|
|
6370
|
-
if (status === 401 || status === 403) {
|
|
6371
|
-
throw new Error("The app refused the ops token (check --token / SPFN_OPS_TOKEN / keychain).");
|
|
6372
|
-
}
|
|
6373
|
-
if (status === 404) {
|
|
6374
|
-
throw new Error("No ops surface at this app (GET /_ops/_manifest answered 404). Mount one with createOpsRouter() and .packages([opsRouter]).");
|
|
6375
|
-
}
|
|
6376
|
-
if (status !== 200) {
|
|
6377
|
-
throw new Error(`Manifest request failed with status ${status}.`);
|
|
6378
|
-
}
|
|
6379
|
-
const manifest = body;
|
|
6380
|
-
if (manifest?.manifestVersion !== 1 || !Array.isArray(manifest.commands)) {
|
|
6381
|
-
throw new Error("The manifest answer has an unknown shape.");
|
|
6382
|
-
}
|
|
6383
|
-
const modules = usableModules(manifest.modules);
|
|
6384
|
-
return {
|
|
6385
|
-
manifestVersion: 1,
|
|
6386
|
-
...manifest.modules !== void 0 ? { modules } : {},
|
|
6387
|
-
commands: usableCommands(manifest.commands, new Set(modules.map((module) => module.id)))
|
|
6388
|
-
};
|
|
6389
|
-
}
|
|
6390
|
-
var OPS_METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
6391
|
-
var OPS_EFFECTS = /* @__PURE__ */ new Set(["read", "write", "destructive"]);
|
|
6392
|
-
var MODULE_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
6393
|
-
var CONTRACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
|
|
6394
|
-
var MAX_ID_LENGTH = 64;
|
|
6395
|
-
var MAX_SOURCE_LENGTH = 128;
|
|
6396
|
-
var MAX_SUMMARY_LENGTH = 500;
|
|
6397
|
-
var MAX_SCOPES = 64;
|
|
6398
|
-
var MAX_SCOPE_LENGTH = 128;
|
|
6399
|
-
var OPS_PATH_PREFIX = "/_ops/";
|
|
6400
|
-
var OPS_PATH_BASE = "https://spfn.invalid";
|
|
6401
|
-
function unstableOpsPathBecause(path5) {
|
|
6342
|
+
// src/surface/command-path.ts
|
|
6343
|
+
var PATH_BASE = "https://spfn.invalid";
|
|
6344
|
+
function unstablePathBecause(path5, prefix) {
|
|
6402
6345
|
let url;
|
|
6403
6346
|
try {
|
|
6404
|
-
url = new URL(path5,
|
|
6347
|
+
url = new URL(path5, PATH_BASE);
|
|
6405
6348
|
} catch {
|
|
6406
6349
|
return "is not a valid URL path";
|
|
6407
6350
|
}
|
|
6408
|
-
if (url.origin !==
|
|
6351
|
+
if (url.origin !== PATH_BASE || url.search !== "" || url.hash !== "") {
|
|
6409
6352
|
return "is not a plain absolute path";
|
|
6410
6353
|
}
|
|
6411
6354
|
if (url.pathname !== path5) {
|
|
6412
6355
|
return "changes when URL-normalized";
|
|
6413
6356
|
}
|
|
6414
|
-
if (!url.pathname.startsWith(
|
|
6415
|
-
return `is outside ${
|
|
6357
|
+
if (!url.pathname.startsWith(prefix)) {
|
|
6358
|
+
return `is outside ${prefix}`;
|
|
6416
6359
|
}
|
|
6417
6360
|
const slashCount = (path5.match(/\//g) ?? []).length;
|
|
6418
6361
|
let decoded = path5;
|
|
@@ -6433,129 +6376,6 @@ function unstableOpsPathBecause(path5) {
|
|
|
6433
6376
|
}
|
|
6434
6377
|
return "uses too many percent-encoding layers";
|
|
6435
6378
|
}
|
|
6436
|
-
function unusableBecause(command) {
|
|
6437
|
-
if (typeof command?.name !== "string" || command.name.length === 0) {
|
|
6438
|
-
return "it has no name";
|
|
6439
|
-
}
|
|
6440
|
-
if (typeof command.method !== "string" || !OPS_METHODS.has(command.method)) {
|
|
6441
|
-
return `its method is ${JSON.stringify(command.method)}`;
|
|
6442
|
-
}
|
|
6443
|
-
if (typeof command.path !== "string") {
|
|
6444
|
-
return `its path ${JSON.stringify(command.path)} is not a string`;
|
|
6445
|
-
}
|
|
6446
|
-
const pathReason = unstableOpsPathBecause(command.path);
|
|
6447
|
-
if (pathReason !== null) {
|
|
6448
|
-
return `its path ${JSON.stringify(command.path)} ${pathReason}`;
|
|
6449
|
-
}
|
|
6450
|
-
return null;
|
|
6451
|
-
}
|
|
6452
|
-
function usableCommands(commands, moduleIds) {
|
|
6453
|
-
const usable = [];
|
|
6454
|
-
for (const command of commands) {
|
|
6455
|
-
const reason = unusableBecause(command);
|
|
6456
|
-
if (reason !== null) {
|
|
6457
|
-
console.error(`\u26A0\uFE0F Ignoring an ops command the manifest announced: ${reason}.`);
|
|
6458
|
-
continue;
|
|
6459
|
-
}
|
|
6460
|
-
const normalized = {
|
|
6461
|
-
name: command.name,
|
|
6462
|
-
method: command.method,
|
|
6463
|
-
path: command.path,
|
|
6464
|
-
input: isPlainObject(command.input) ? command.input : {}
|
|
6465
|
-
};
|
|
6466
|
-
const metadataReason = unusableModuleMetadataBecause(command, moduleIds);
|
|
6467
|
-
if (metadataReason === null && command.module !== void 0) {
|
|
6468
|
-
normalized.module = command.module;
|
|
6469
|
-
normalized.summary = command.summary;
|
|
6470
|
-
normalized.effect = command.effect;
|
|
6471
|
-
normalized.scopes = [...command.scopes];
|
|
6472
|
-
} else if (metadataReason !== null) {
|
|
6473
|
-
normalized.metadataRejected = true;
|
|
6474
|
-
console.error(
|
|
6475
|
-
`\u26A0\uFE0F Ignoring module metadata for ops command "${plain(command.name)}": ${metadataReason}. Calling it will require --yes.`
|
|
6476
|
-
);
|
|
6477
|
-
}
|
|
6478
|
-
usable.push(normalized);
|
|
6479
|
-
}
|
|
6480
|
-
return usable;
|
|
6481
|
-
}
|
|
6482
|
-
function unusableModuleMetadataBecause(command, moduleIds) {
|
|
6483
|
-
const carriesMetadata = command.module !== void 0 || command.summary !== void 0 || command.effect !== void 0 || command.scopes !== void 0;
|
|
6484
|
-
if (!carriesMetadata) {
|
|
6485
|
-
return null;
|
|
6486
|
-
}
|
|
6487
|
-
if (typeof command.module !== "string" || !moduleIds.has(command.module)) {
|
|
6488
|
-
return "it names no usable module";
|
|
6489
|
-
}
|
|
6490
|
-
if (!command.name.startsWith(`${command.module}.`)) {
|
|
6491
|
-
return "its qualified name does not start with the module id";
|
|
6492
|
-
}
|
|
6493
|
-
if (!command.path.startsWith(`${OPS_PATH_PREFIX}${command.module}/`)) {
|
|
6494
|
-
return "its path is outside the module namespace";
|
|
6495
|
-
}
|
|
6496
|
-
if (typeof command.summary !== "string" || command.summary.length === 0 || command.summary.length > MAX_SUMMARY_LENGTH) {
|
|
6497
|
-
return "its summary is missing or too long";
|
|
6498
|
-
}
|
|
6499
|
-
if (typeof command.effect !== "string" || !OPS_EFFECTS.has(command.effect)) {
|
|
6500
|
-
return "its effect is unknown";
|
|
6501
|
-
}
|
|
6502
|
-
if (!Array.isArray(command.scopes) || command.scopes.length === 0 || command.scopes.length > MAX_SCOPES || command.scopes.some((scope) => typeof scope !== "string" || scope.length === 0 || scope.length > MAX_SCOPE_LENGTH)) {
|
|
6503
|
-
return "its scopes are missing or invalid";
|
|
6504
|
-
}
|
|
6505
|
-
return null;
|
|
6506
|
-
}
|
|
6507
|
-
function usableModules(value) {
|
|
6508
|
-
if (value === void 0) {
|
|
6509
|
-
return [];
|
|
6510
|
-
}
|
|
6511
|
-
if (!Array.isArray(value)) {
|
|
6512
|
-
console.error("\u26A0\uFE0F Ignoring ops module metadata: modules is not an array.");
|
|
6513
|
-
return [];
|
|
6514
|
-
}
|
|
6515
|
-
const modules = [];
|
|
6516
|
-
const claimed = /* @__PURE__ */ new Set();
|
|
6517
|
-
for (const raw of value) {
|
|
6518
|
-
const reason = unusableModuleBecause(raw, claimed);
|
|
6519
|
-
if (reason !== null) {
|
|
6520
|
-
console.error(`\u26A0\uFE0F Ignoring an ops module the manifest announced: ${reason}.`);
|
|
6521
|
-
continue;
|
|
6522
|
-
}
|
|
6523
|
-
const module = raw;
|
|
6524
|
-
claimed.add(module.id);
|
|
6525
|
-
modules.push({
|
|
6526
|
-
id: module.id,
|
|
6527
|
-
source: module.source,
|
|
6528
|
-
contractVersion: module.contractVersion,
|
|
6529
|
-
summary: module.summary
|
|
6530
|
-
});
|
|
6531
|
-
}
|
|
6532
|
-
return modules;
|
|
6533
|
-
}
|
|
6534
|
-
function unusableModuleBecause(value, claimed) {
|
|
6535
|
-
if (!isPlainObject(value)) {
|
|
6536
|
-
return "it is not an object";
|
|
6537
|
-
}
|
|
6538
|
-
const id = value.id;
|
|
6539
|
-
if (typeof id !== "string" || id.length > MAX_ID_LENGTH || !MODULE_ID.test(id)) {
|
|
6540
|
-
return "its id is invalid";
|
|
6541
|
-
}
|
|
6542
|
-
if (claimed.has(id)) {
|
|
6543
|
-
return `id "${id}" is duplicated`;
|
|
6544
|
-
}
|
|
6545
|
-
if (typeof value.source !== "string" || value.source.length === 0 || value.source.length > MAX_SOURCE_LENGTH) {
|
|
6546
|
-
return `module "${id}" has an invalid source`;
|
|
6547
|
-
}
|
|
6548
|
-
if (typeof value.contractVersion !== "string" || !CONTRACT_VERSION.test(value.contractVersion)) {
|
|
6549
|
-
return `module "${id}" has an invalid contract version`;
|
|
6550
|
-
}
|
|
6551
|
-
if (typeof value.summary !== "string" || value.summary.length === 0 || value.summary.length > MAX_SUMMARY_LENGTH) {
|
|
6552
|
-
return `module "${id}" has an invalid summary`;
|
|
6553
|
-
}
|
|
6554
|
-
return null;
|
|
6555
|
-
}
|
|
6556
|
-
function isPlainObject(value) {
|
|
6557
|
-
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6558
|
-
}
|
|
6559
6379
|
function climbsOutOfItsSegment(value) {
|
|
6560
6380
|
let decoded = value;
|
|
6561
6381
|
for (let depth = 0; depth < 8; depth++) {
|
|
@@ -6575,10 +6395,10 @@ function climbsOutOfItsSegment(value) {
|
|
|
6575
6395
|
}
|
|
6576
6396
|
return true;
|
|
6577
6397
|
}
|
|
6578
|
-
function buildCommandPath(command, params, query) {
|
|
6579
|
-
const templateReason =
|
|
6398
|
+
function buildCommandPath(command, params, query, namespace) {
|
|
6399
|
+
const templateReason = unstablePathBecause(command.path, `${namespace}/`);
|
|
6580
6400
|
if (templateReason !== null) {
|
|
6581
|
-
throw new Error(`Refusing
|
|
6401
|
+
throw new Error(`Refusing command path ${JSON.stringify(command.path)} because it ${templateReason}.`);
|
|
6582
6402
|
}
|
|
6583
6403
|
const path5 = command.path.replace(/:([A-Za-z0-9_]+)/g, (_match, name) => {
|
|
6584
6404
|
const value = params[name];
|
|
@@ -6598,12 +6418,13 @@ function buildCommandPath(command, params, query) {
|
|
|
6598
6418
|
const search = new URLSearchParams(query).toString();
|
|
6599
6419
|
return search.length > 0 ? `${path5}?${search}` : path5;
|
|
6600
6420
|
}
|
|
6601
|
-
|
|
6602
|
-
|
|
6603
|
-
|
|
6421
|
+
|
|
6422
|
+
// src/surface/plain.ts
|
|
6423
|
+
function plain(value) {
|
|
6424
|
+
return value.replace(/[\u0000-\u001f\u007f-\u009f]/g, "?");
|
|
6604
6425
|
}
|
|
6605
6426
|
|
|
6606
|
-
// src/
|
|
6427
|
+
// src/surface/describe.ts
|
|
6607
6428
|
var SECTIONS = [
|
|
6608
6429
|
{ key: "params", label: "path parameters", flag: "--param" },
|
|
6609
6430
|
{ key: "query", label: "query parameters", flag: "--query" },
|
|
@@ -6695,7 +6516,7 @@ function renderSection(label, flag, rows) {
|
|
|
6695
6516
|
}
|
|
6696
6517
|
return lines;
|
|
6697
6518
|
}
|
|
6698
|
-
function renderCommandUsage(command) {
|
|
6519
|
+
function renderCommandUsage(command, invoke) {
|
|
6699
6520
|
const lines = [`${plain(command.name)} ${command.method} ${plain(command.path)}`, ""];
|
|
6700
6521
|
let described = false;
|
|
6701
6522
|
if (command.summary) {
|
|
@@ -6720,8 +6541,10 @@ function renderCommandUsage(command) {
|
|
|
6720
6541
|
if (!described) {
|
|
6721
6542
|
lines.push(" Takes no input.", "");
|
|
6722
6543
|
}
|
|
6723
|
-
|
|
6724
|
-
|
|
6544
|
+
if (invoke !== void 0) {
|
|
6545
|
+
lines.push(` Invoke: ${invoke} ${plain(command.name)}${exampleFlags(command)}`);
|
|
6546
|
+
}
|
|
6547
|
+
return lines.join("\n").replace(/\n+$/, "");
|
|
6725
6548
|
}
|
|
6726
6549
|
function exampleFlags(command) {
|
|
6727
6550
|
const parts = [];
|
|
@@ -6739,6 +6562,324 @@ function exampleFlags(command) {
|
|
|
6739
6562
|
return parts.join("");
|
|
6740
6563
|
}
|
|
6741
6564
|
|
|
6565
|
+
// src/surface/manifest-trust.ts
|
|
6566
|
+
function trustedManifest(body, reader) {
|
|
6567
|
+
const manifest = body;
|
|
6568
|
+
if (manifest?.manifestVersion !== 1 || !Array.isArray(manifest.commands)) {
|
|
6569
|
+
throw new Error("The manifest answer has an unknown shape.");
|
|
6570
|
+
}
|
|
6571
|
+
const modules = usableModules(manifest.modules, reader);
|
|
6572
|
+
return {
|
|
6573
|
+
manifestVersion: 1,
|
|
6574
|
+
...manifest.modules !== void 0 ? { modules } : {},
|
|
6575
|
+
commands: usableCommands(manifest.commands, new Set(modules.map((module) => module.id)), reader)
|
|
6576
|
+
};
|
|
6577
|
+
}
|
|
6578
|
+
var METHODS = /* @__PURE__ */ new Set(["GET", "POST", "PUT", "PATCH", "DELETE"]);
|
|
6579
|
+
var EFFECTS = /* @__PURE__ */ new Set(["read", "write", "destructive"]);
|
|
6580
|
+
var MODULE_ID = /^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$/;
|
|
6581
|
+
var CONTRACT_VERSION = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)*$/;
|
|
6582
|
+
var MAX_ID_LENGTH = 64;
|
|
6583
|
+
var MAX_SOURCE_LENGTH = 128;
|
|
6584
|
+
var MAX_SUMMARY_LENGTH = 500;
|
|
6585
|
+
var MAX_SCOPES = 64;
|
|
6586
|
+
var MAX_SCOPE_LENGTH = 128;
|
|
6587
|
+
function unusableBecause(command, reader) {
|
|
6588
|
+
if (typeof command?.name !== "string" || command.name.length === 0) {
|
|
6589
|
+
return "it has no name";
|
|
6590
|
+
}
|
|
6591
|
+
if (typeof command.method !== "string" || !METHODS.has(command.method)) {
|
|
6592
|
+
return `its method is ${JSON.stringify(command.method)}`;
|
|
6593
|
+
}
|
|
6594
|
+
if (typeof command.path !== "string") {
|
|
6595
|
+
return `its path ${JSON.stringify(command.path)} is not a string`;
|
|
6596
|
+
}
|
|
6597
|
+
const pathReason = unstablePathBecause(command.path, reader.prefix);
|
|
6598
|
+
if (pathReason !== null) {
|
|
6599
|
+
return `its path ${JSON.stringify(command.path)} ${pathReason}`;
|
|
6600
|
+
}
|
|
6601
|
+
return null;
|
|
6602
|
+
}
|
|
6603
|
+
function usableCommands(commands, moduleIds, reader) {
|
|
6604
|
+
const usable = [];
|
|
6605
|
+
for (const command of commands) {
|
|
6606
|
+
const reason = unusableBecause(command, reader);
|
|
6607
|
+
if (reason !== null) {
|
|
6608
|
+
reader.warn(
|
|
6609
|
+
`Ignoring a command the ${reader.namespace} manifest announced: ${reason}.`,
|
|
6610
|
+
{ kind: "command", reason }
|
|
6611
|
+
);
|
|
6612
|
+
continue;
|
|
6613
|
+
}
|
|
6614
|
+
const normalized = {
|
|
6615
|
+
name: command.name,
|
|
6616
|
+
method: command.method,
|
|
6617
|
+
path: command.path,
|
|
6618
|
+
input: isPlainObject(command.input) ? command.input : {}
|
|
6619
|
+
};
|
|
6620
|
+
const metadataReason = unusableModuleMetadataBecause(command, moduleIds, reader);
|
|
6621
|
+
if (metadataReason === null && command.module !== void 0) {
|
|
6622
|
+
normalized.module = command.module;
|
|
6623
|
+
normalized.summary = command.summary;
|
|
6624
|
+
normalized.effect = command.effect;
|
|
6625
|
+
normalized.scopes = [...command.scopes];
|
|
6626
|
+
} else if (metadataReason !== null) {
|
|
6627
|
+
normalized.metadataRejected = true;
|
|
6628
|
+
reader.warn(
|
|
6629
|
+
`Ignoring module metadata for command "${plain(command.name)}": ${metadataReason}. Its effect is unknown, so treat it as destructive.`,
|
|
6630
|
+
{ kind: "metadata", command: command.name, reason: metadataReason }
|
|
6631
|
+
);
|
|
6632
|
+
}
|
|
6633
|
+
usable.push(normalized);
|
|
6634
|
+
}
|
|
6635
|
+
return usable;
|
|
6636
|
+
}
|
|
6637
|
+
function unusableModuleMetadataBecause(command, moduleIds, reader) {
|
|
6638
|
+
const carriesMetadata = command.module !== void 0 || command.summary !== void 0 || command.effect !== void 0 || command.scopes !== void 0;
|
|
6639
|
+
if (!carriesMetadata) {
|
|
6640
|
+
return null;
|
|
6641
|
+
}
|
|
6642
|
+
if (typeof command.module !== "string" || !moduleIds.has(command.module)) {
|
|
6643
|
+
return "it names no usable module";
|
|
6644
|
+
}
|
|
6645
|
+
if (!command.name.startsWith(`${command.module}.`)) {
|
|
6646
|
+
return "its qualified name does not start with the module id";
|
|
6647
|
+
}
|
|
6648
|
+
if (!command.path.startsWith(`${reader.prefix}${command.module}/`)) {
|
|
6649
|
+
return "its path is outside the module namespace";
|
|
6650
|
+
}
|
|
6651
|
+
if (typeof command.summary !== "string" || command.summary.length === 0 || command.summary.length > MAX_SUMMARY_LENGTH) {
|
|
6652
|
+
return "its summary is missing or too long";
|
|
6653
|
+
}
|
|
6654
|
+
if (typeof command.effect !== "string" || !EFFECTS.has(command.effect)) {
|
|
6655
|
+
return "its effect is unknown";
|
|
6656
|
+
}
|
|
6657
|
+
if (!Array.isArray(command.scopes) || command.scopes.length === 0 || command.scopes.length > MAX_SCOPES || command.scopes.some((scope) => typeof scope !== "string" || scope.length === 0 || scope.length > MAX_SCOPE_LENGTH)) {
|
|
6658
|
+
return "its scopes are missing or invalid";
|
|
6659
|
+
}
|
|
6660
|
+
return null;
|
|
6661
|
+
}
|
|
6662
|
+
function usableModules(value, reader) {
|
|
6663
|
+
if (value === void 0) {
|
|
6664
|
+
return [];
|
|
6665
|
+
}
|
|
6666
|
+
if (!Array.isArray(value)) {
|
|
6667
|
+
reader.warn("Ignoring module metadata: modules is not an array.", {
|
|
6668
|
+
kind: "modules",
|
|
6669
|
+
reason: "modules is not an array"
|
|
6670
|
+
});
|
|
6671
|
+
return [];
|
|
6672
|
+
}
|
|
6673
|
+
const modules = [];
|
|
6674
|
+
const claimed = /* @__PURE__ */ new Set();
|
|
6675
|
+
for (const raw of value) {
|
|
6676
|
+
const reason = unusableModuleBecause(raw, claimed);
|
|
6677
|
+
if (reason !== null) {
|
|
6678
|
+
reader.warn(
|
|
6679
|
+
`Ignoring a module the ${reader.namespace} manifest announced: ${reason}.`,
|
|
6680
|
+
{ kind: "module", reason }
|
|
6681
|
+
);
|
|
6682
|
+
continue;
|
|
6683
|
+
}
|
|
6684
|
+
const module = raw;
|
|
6685
|
+
claimed.add(module.id);
|
|
6686
|
+
modules.push({
|
|
6687
|
+
id: module.id,
|
|
6688
|
+
source: module.source,
|
|
6689
|
+
contractVersion: module.contractVersion,
|
|
6690
|
+
summary: module.summary
|
|
6691
|
+
});
|
|
6692
|
+
}
|
|
6693
|
+
return modules;
|
|
6694
|
+
}
|
|
6695
|
+
function unusableModuleBecause(value, claimed) {
|
|
6696
|
+
if (!isPlainObject(value)) {
|
|
6697
|
+
return "it is not an object";
|
|
6698
|
+
}
|
|
6699
|
+
const id = value.id;
|
|
6700
|
+
if (typeof id !== "string" || id.length > MAX_ID_LENGTH || !MODULE_ID.test(id)) {
|
|
6701
|
+
return "its id is invalid";
|
|
6702
|
+
}
|
|
6703
|
+
if (claimed.has(id)) {
|
|
6704
|
+
return `id "${id}" is duplicated`;
|
|
6705
|
+
}
|
|
6706
|
+
if (typeof value.source !== "string" || value.source.length === 0 || value.source.length > MAX_SOURCE_LENGTH) {
|
|
6707
|
+
return `module "${id}" has an invalid source`;
|
|
6708
|
+
}
|
|
6709
|
+
if (typeof value.contractVersion !== "string" || !CONTRACT_VERSION.test(value.contractVersion)) {
|
|
6710
|
+
return `module "${id}" has an invalid contract version`;
|
|
6711
|
+
}
|
|
6712
|
+
if (typeof value.summary !== "string" || value.summary.length === 0 || value.summary.length > MAX_SUMMARY_LENGTH) {
|
|
6713
|
+
return `module "${id}" has an invalid summary`;
|
|
6714
|
+
}
|
|
6715
|
+
return null;
|
|
6716
|
+
}
|
|
6717
|
+
function isPlainObject(value) {
|
|
6718
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6719
|
+
}
|
|
6720
|
+
|
|
6721
|
+
// src/surface/transport.ts
|
|
6722
|
+
function isLoopback(hostname3) {
|
|
6723
|
+
return hostname3 === "localhost" || hostname3 === "127.0.0.1" || hostname3 === "[::1]" || hostname3.endsWith(".localhost");
|
|
6724
|
+
}
|
|
6725
|
+
function transportRefusalOf(url) {
|
|
6726
|
+
if (url.protocol === "https:" || url.protocol === "http:" && isLoopback(url.hostname)) {
|
|
6727
|
+
return null;
|
|
6728
|
+
}
|
|
6729
|
+
return url.protocol === "http:" ? "cleartext-http" : "not-http";
|
|
6730
|
+
}
|
|
6731
|
+
function parseAppUrl(appUrl) {
|
|
6732
|
+
try {
|
|
6733
|
+
return new URL(appUrl);
|
|
6734
|
+
} catch {
|
|
6735
|
+
return null;
|
|
6736
|
+
}
|
|
6737
|
+
}
|
|
6738
|
+
function assertTransportSafe(appUrl) {
|
|
6739
|
+
const url = parseAppUrl(appUrl);
|
|
6740
|
+
if (url === null) {
|
|
6741
|
+
throw new Error(`Invalid app URL: ${appUrl}`);
|
|
6742
|
+
}
|
|
6743
|
+
const refusal = transportRefusalOf(url);
|
|
6744
|
+
if (refusal === "cleartext-http") {
|
|
6745
|
+
throw new Error(`Refusing to talk to ${appUrl} over http: a credential would cross it in the clear. Use https. http is allowed only against localhost.`);
|
|
6746
|
+
}
|
|
6747
|
+
if (refusal === "not-http") {
|
|
6748
|
+
throw new Error(`App URL must be https, got "${url.protocol}//". http is allowed only against localhost.`);
|
|
6749
|
+
}
|
|
6750
|
+
}
|
|
6751
|
+
|
|
6752
|
+
// src/surface/client.ts
|
|
6753
|
+
var NAMESPACE = /^(?:\/[A-Za-z0-9_-]+)+$/;
|
|
6754
|
+
var SurfaceManifestError = class extends Error {
|
|
6755
|
+
constructor(message, status) {
|
|
6756
|
+
super(message);
|
|
6757
|
+
this.status = status;
|
|
6758
|
+
this.name = "SurfaceManifestError";
|
|
6759
|
+
}
|
|
6760
|
+
};
|
|
6761
|
+
function joinUrl(appUrl, path5) {
|
|
6762
|
+
return appUrl.replace(/\/+$/, "") + path5;
|
|
6763
|
+
}
|
|
6764
|
+
async function request(appUrl, token, method, path5, body) {
|
|
6765
|
+
const response = await fetch(joinUrl(appUrl, path5), {
|
|
6766
|
+
method,
|
|
6767
|
+
headers: {
|
|
6768
|
+
Authorization: `Bearer ${token}`,
|
|
6769
|
+
...body !== void 0 ? { "Content-Type": "application/json" } : {}
|
|
6770
|
+
},
|
|
6771
|
+
...body !== void 0 ? { body: JSON.stringify(body) } : {}
|
|
6772
|
+
});
|
|
6773
|
+
const text = await response.text();
|
|
6774
|
+
let parsed = text;
|
|
6775
|
+
try {
|
|
6776
|
+
parsed = text.length > 0 ? JSON.parse(text) : null;
|
|
6777
|
+
} catch {
|
|
6778
|
+
}
|
|
6779
|
+
return { status: response.status, body: parsed };
|
|
6780
|
+
}
|
|
6781
|
+
function manifestRefusal(namespace, status) {
|
|
6782
|
+
if (status === 401 || status === 403) {
|
|
6783
|
+
return new SurfaceManifestError(`The app refused the token (GET ${namespace}/_manifest answered ${status}).`, status);
|
|
6784
|
+
}
|
|
6785
|
+
if (status === 404) {
|
|
6786
|
+
return new SurfaceManifestError(`No surface at ${namespace} on this app (GET ${namespace}/_manifest answered 404).`, status);
|
|
6787
|
+
}
|
|
6788
|
+
return new SurfaceManifestError(`Manifest request failed with status ${status}.`, status);
|
|
6789
|
+
}
|
|
6790
|
+
function assertNamespace(namespace) {
|
|
6791
|
+
if (typeof namespace !== "string" || !NAMESPACE.test(namespace)) {
|
|
6792
|
+
throw new Error(
|
|
6793
|
+
`Surface namespace ${JSON.stringify(namespace)} is invalid. Use an absolute path of [A-Za-z0-9_-] segments without a trailing slash, such as "/_app".`
|
|
6794
|
+
);
|
|
6795
|
+
}
|
|
6796
|
+
}
|
|
6797
|
+
function findCommand(manifest, name) {
|
|
6798
|
+
const command = manifest.commands.find((candidate) => candidate.name === name);
|
|
6799
|
+
if (!command) {
|
|
6800
|
+
throw new Error(`Unknown command "${plain(name)}".`);
|
|
6801
|
+
}
|
|
6802
|
+
return command;
|
|
6803
|
+
}
|
|
6804
|
+
function createSurfaceClient(options) {
|
|
6805
|
+
assertTransportSafe(options.appUrl);
|
|
6806
|
+
assertNamespace(options.namespace);
|
|
6807
|
+
const { appUrl, namespace } = options;
|
|
6808
|
+
const token = async () => typeof options.token === "function" ? await options.token() : options.token;
|
|
6809
|
+
const reader = { namespace, prefix: `${namespace}/`, warn: options.onWarning ?? (() => void 0) };
|
|
6810
|
+
const manifest = async () => {
|
|
6811
|
+
const { status, body } = await request(appUrl, await token(), "GET", `${namespace}/_manifest`);
|
|
6812
|
+
if (status !== 200) {
|
|
6813
|
+
throw manifestRefusal(namespace, status);
|
|
6814
|
+
}
|
|
6815
|
+
return trustedManifest(body, reader);
|
|
6816
|
+
};
|
|
6817
|
+
const invoke = async (command, input) => {
|
|
6818
|
+
const path5 = buildCommandPath(command, input.params, input.query, namespace);
|
|
6819
|
+
return await request(appUrl, await token(), command.method, path5, input.body);
|
|
6820
|
+
};
|
|
6821
|
+
return {
|
|
6822
|
+
manifest,
|
|
6823
|
+
invoke,
|
|
6824
|
+
describe: (fetched, name) => renderCommandUsage(findCommand(fetched, name)),
|
|
6825
|
+
call: async (name, input) => await invoke(findCommand(await manifest(), name), input)
|
|
6826
|
+
};
|
|
6827
|
+
}
|
|
6828
|
+
|
|
6829
|
+
// src/utils/ops/client.ts
|
|
6830
|
+
var OPS_NAMESPACE = "/_ops";
|
|
6831
|
+
function printOpsWarning(_message, warning) {
|
|
6832
|
+
switch (warning.kind) {
|
|
6833
|
+
case "command":
|
|
6834
|
+
console.error(`\u26A0\uFE0F Ignoring an ops command the manifest announced: ${warning.reason}.`);
|
|
6835
|
+
break;
|
|
6836
|
+
case "metadata":
|
|
6837
|
+
console.error(
|
|
6838
|
+
`\u26A0\uFE0F Ignoring module metadata for ops command "${plain(warning.command)}": ${warning.reason}. Calling it will require --yes.`
|
|
6839
|
+
);
|
|
6840
|
+
break;
|
|
6841
|
+
case "modules":
|
|
6842
|
+
console.error(`\u26A0\uFE0F Ignoring ops module metadata: ${warning.reason}.`);
|
|
6843
|
+
break;
|
|
6844
|
+
case "module":
|
|
6845
|
+
console.error(`\u26A0\uFE0F Ignoring an ops module the manifest announced: ${warning.reason}.`);
|
|
6846
|
+
break;
|
|
6847
|
+
}
|
|
6848
|
+
}
|
|
6849
|
+
function opsClient(appUrl, token) {
|
|
6850
|
+
return createSurfaceClient({ appUrl, namespace: OPS_NAMESPACE, token, onWarning: printOpsWarning });
|
|
6851
|
+
}
|
|
6852
|
+
function inOpsTerms(error) {
|
|
6853
|
+
if (!(error instanceof SurfaceManifestError)) {
|
|
6854
|
+
return error;
|
|
6855
|
+
}
|
|
6856
|
+
if (error.status === 401 || error.status === 403) {
|
|
6857
|
+
return new Error("The app refused the ops token (check --token / SPFN_OPS_TOKEN / keychain).");
|
|
6858
|
+
}
|
|
6859
|
+
if (error.status === 404) {
|
|
6860
|
+
return new Error("No ops surface at this app (GET /_ops/_manifest answered 404). Mount one with createOpsRouter() and .packages([opsRouter]).");
|
|
6861
|
+
}
|
|
6862
|
+
return error;
|
|
6863
|
+
}
|
|
6864
|
+
async function fetchOpsManifest(appUrl, token) {
|
|
6865
|
+
return await opsClient(appUrl, token).manifest().catch((error) => Promise.reject(inOpsTerms(error)));
|
|
6866
|
+
}
|
|
6867
|
+
function assertOpsCommandPath(command) {
|
|
6868
|
+
const templateReason = unstablePathBecause(command.path, `${OPS_NAMESPACE}/`);
|
|
6869
|
+
if (templateReason !== null) {
|
|
6870
|
+
throw new Error(`Refusing ops command path ${JSON.stringify(command.path)} because it ${templateReason}.`);
|
|
6871
|
+
}
|
|
6872
|
+
}
|
|
6873
|
+
async function invokeOpsCommand(appUrl, token, command, input) {
|
|
6874
|
+
assertOpsCommandPath(command);
|
|
6875
|
+
return await opsClient(appUrl, token).invoke(command, input);
|
|
6876
|
+
}
|
|
6877
|
+
|
|
6878
|
+
// src/utils/ops/describe.ts
|
|
6879
|
+
function renderCommandUsage2(command) {
|
|
6880
|
+
return renderCommandUsage(command, "spfn ops call");
|
|
6881
|
+
}
|
|
6882
|
+
|
|
6742
6883
|
// src/commands/ops/resolve.ts
|
|
6743
6884
|
import chalk33 from "chalk";
|
|
6744
6885
|
|
|
@@ -6811,14 +6952,12 @@ function deleteOpsToken(account) {
|
|
|
6811
6952
|
}
|
|
6812
6953
|
|
|
6813
6954
|
// src/commands/ops/resolve.ts
|
|
6814
|
-
function
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
function assertTransportSafe(parsed, appUrl) {
|
|
6818
|
-
if (parsed.protocol === "https:" || parsed.protocol === "http:" && isLoopback(parsed.hostname)) {
|
|
6955
|
+
function assertTransportSafe2(parsed, appUrl) {
|
|
6956
|
+
const refusal = transportRefusalOf(parsed);
|
|
6957
|
+
if (refusal === null) {
|
|
6819
6958
|
return;
|
|
6820
6959
|
}
|
|
6821
|
-
if (
|
|
6960
|
+
if (refusal === "cleartext-http") {
|
|
6822
6961
|
console.error(chalk33.red(`\u274C Refusing to talk to ${appUrl} over http.`));
|
|
6823
6962
|
console.error(chalk33.gray(" An administrator password and an ops token cross this connection, and http sends both in the clear."));
|
|
6824
6963
|
console.error(chalk33.gray(" Use https. http is allowed only against localhost."));
|
|
@@ -6841,7 +6980,7 @@ function resolveAppUrl(options) {
|
|
|
6841
6980
|
console.error(chalk33.red(`\u274C Invalid app URL: ${appUrl}`));
|
|
6842
6981
|
process.exit(1);
|
|
6843
6982
|
}
|
|
6844
|
-
|
|
6983
|
+
assertTransportSafe2(parsed, appUrl);
|
|
6845
6984
|
return appUrl;
|
|
6846
6985
|
}
|
|
6847
6986
|
function appAccount(appUrl) {
|
|
@@ -7245,7 +7384,7 @@ async function callCommand(name, options) {
|
|
|
7245
7384
|
process.exit(1);
|
|
7246
7385
|
}
|
|
7247
7386
|
if (options.describe) {
|
|
7248
|
-
console.log(options.json ? JSON.stringify(command, null, 2) :
|
|
7387
|
+
console.log(options.json ? JSON.stringify(command, null, 2) : renderCommandUsage2(command));
|
|
7249
7388
|
return;
|
|
7250
7389
|
}
|
|
7251
7390
|
if (destructiveConfirmationRequired(command, options.yes)) {
|
|
@@ -7514,15 +7653,15 @@ function targetLayer(target) {
|
|
|
7514
7653
|
}
|
|
7515
7654
|
function entriesForTarget(entries, target) {
|
|
7516
7655
|
const layer = targetLayer(target);
|
|
7517
|
-
return layer ? entries.filter((entry) =>
|
|
7656
|
+
return layer ? entries.filter((entry) => (entry.layer ?? "environment") === layer) : entries;
|
|
7518
7657
|
}
|
|
7519
7658
|
function assertTargetLayer(entry, target) {
|
|
7520
7659
|
const layer = targetLayer(target);
|
|
7521
|
-
if (!entry || !layer ||
|
|
7660
|
+
if (!entry || !layer || allowsLayer(entry, layer)) {
|
|
7522
7661
|
return;
|
|
7523
7662
|
}
|
|
7524
7663
|
const hint = layer === "environment" ? "pass --instance <name>" : "drop --instance";
|
|
7525
|
-
logger.error(`${entry.key} is declared with layer "${
|
|
7664
|
+
logger.error(`${entry.key} is declared with layer "${entry.layer}" \u2014 ${hint}.`);
|
|
7526
7665
|
process.exit(1);
|
|
7527
7666
|
}
|
|
7528
7667
|
|
|
@@ -7946,6 +8085,7 @@ async function secretCheck(options) {
|
|
|
7946
8085
|
}
|
|
7947
8086
|
function checkPlacement(cwd, schema, target, issues, warnings) {
|
|
7948
8087
|
const files = [{ file: getSopsFile(cwd, target.env), layer: "environment" }];
|
|
8088
|
+
const seenIn = /* @__PURE__ */ new Map();
|
|
7949
8089
|
if (target.instance) {
|
|
7950
8090
|
files.push({ file: getSopsFile(cwd, target.env, target.instance), layer: "instance" });
|
|
7951
8091
|
}
|
|
@@ -7954,9 +8094,12 @@ function checkPlacement(cwd, schema, target, issues, warnings) {
|
|
|
7954
8094
|
const entry = Object.hasOwn(schema, name) ? schema[name] : void 0;
|
|
7955
8095
|
if (!entry) {
|
|
7956
8096
|
warnings.push(`${chalk43.cyan(name)} in ${chalk43.yellow(file.relFile)} is not in the env list \u2014 export skips it.`);
|
|
7957
|
-
} else if (
|
|
7958
|
-
issues.push(`${chalk43.cyan(name)} is declared with layer "${
|
|
8097
|
+
} else if (!allowsLayer(entry, layer)) {
|
|
8098
|
+
issues.push(`${chalk43.cyan(name)} is declared with layer "${entry.layer}" but is in ${chalk43.yellow(file.relFile)}.`);
|
|
8099
|
+
} else if (seenIn.has(name)) {
|
|
8100
|
+
issues.push(`${chalk43.cyan(name)} is in both ${chalk43.yellow(seenIn.get(name))} and ${chalk43.yellow(file.relFile)}.`);
|
|
7959
8101
|
}
|
|
8102
|
+
seenIn.set(name, file.relFile);
|
|
7960
8103
|
}
|
|
7961
8104
|
}
|
|
7962
8105
|
}
|
|
@@ -8023,8 +8166,8 @@ function placementError(entry, found) {
|
|
|
8023
8166
|
if (found.length > 1) {
|
|
8024
8167
|
return `in more than one layer: ${found.map((source) => source.label).join(" and ")}`;
|
|
8025
8168
|
}
|
|
8026
|
-
if (found[0].kind
|
|
8027
|
-
return `declared with layer "${
|
|
8169
|
+
if (!allowsLayer(entry, found[0].kind)) {
|
|
8170
|
+
return `declared with layer "${entry.layer}", but found in ${found[0].label}`;
|
|
8028
8171
|
}
|
|
8029
8172
|
return void 0;
|
|
8030
8173
|
}
|