@specbook/cli 0.1.0 → 0.2.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 +1 -1
- package/dist/bin.js +271 -94
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -4,4 +4,4 @@ Specbook command line: init, add, generate, dev, build, run, check, secrets and
|
|
|
4
4
|
|
|
5
5
|
Part of [Specbook](https://github.com/specbook-dev/specbook), Storybook for APIs. See the repository for documentation.
|
|
6
6
|
|
|
7
|
-
Source-available under the [Functional Source License (FSL-1.1-MIT)](LICENSE)
|
|
7
|
+
Source-available under the [Functional Source License (FSL-1.1-MIT)](LICENSE). Free to use, including at work and in commercial projects; the only restriction is offering it, or something substantially similar built from it, to others as a competing commercial product or service. Each version becomes MIT two years after its release.
|
package/dist/bin.js
CHANGED
|
@@ -3192,8 +3192,32 @@ function createFetchTransport(opts) {
|
|
|
3192
3192
|
|
|
3193
3193
|
// ../core/src/spec-config.ts
|
|
3194
3194
|
var isUrl = (s) => /^https?:\/\//i.test(s);
|
|
3195
|
+
function discovery(s, base) {
|
|
3196
|
+
if (typeof s.openIdConnectUrl !== "string" || s.openIdConnectUrl === "") return void 0;
|
|
3197
|
+
try {
|
|
3198
|
+
return { openIdConnectUrl: new URL(s.openIdConnectUrl, base).toString() };
|
|
3199
|
+
} catch {
|
|
3200
|
+
return void 0;
|
|
3201
|
+
}
|
|
3202
|
+
}
|
|
3203
|
+
function codeFlow(s, base) {
|
|
3204
|
+
const flow = s.flows?.authorizationCode ?? (s.flow === "accessCode" ? s : void 0);
|
|
3205
|
+
if (!flow?.authorizationUrl || !flow?.tokenUrl) return void 0;
|
|
3206
|
+
const abs = (u) => {
|
|
3207
|
+
try {
|
|
3208
|
+
return new URL(u, base).toString();
|
|
3209
|
+
} catch {
|
|
3210
|
+
return u;
|
|
3211
|
+
}
|
|
3212
|
+
};
|
|
3213
|
+
return {
|
|
3214
|
+
authorizationUrl: abs(flow.authorizationUrl),
|
|
3215
|
+
tokenUrl: abs(flow.tokenUrl),
|
|
3216
|
+
...flow.scopes && Object.keys(flow.scopes).length ? { scopes: flow.scopes } : {}
|
|
3217
|
+
};
|
|
3218
|
+
}
|
|
3195
3219
|
var secretName = (scheme) => scheme.replace(/[^A-Za-z0-9]+/g, "_").replace(/^_+|_+$/g, "").toUpperCase() || "API_KEY";
|
|
3196
|
-
function authFromSpec(api) {
|
|
3220
|
+
function authFromSpec(api, source) {
|
|
3197
3221
|
const schemes = api.components?.securitySchemes ?? api.securityDefinitions ?? {};
|
|
3198
3222
|
const out = { auth: {}, secrets: {} };
|
|
3199
3223
|
for (const [name, s] of Object.entries(schemes)) {
|
|
@@ -3201,6 +3225,7 @@ function authFromSpec(api) {
|
|
|
3201
3225
|
const ref = `{{secrets.${secret}}}`;
|
|
3202
3226
|
let apply;
|
|
3203
3227
|
let title = name;
|
|
3228
|
+
let oauth;
|
|
3204
3229
|
if (s.type === "apiKey") {
|
|
3205
3230
|
const key = s.name;
|
|
3206
3231
|
if (s.in === "header") apply = { headers: { [key]: ref } };
|
|
@@ -3218,10 +3243,12 @@ function authFromSpec(api) {
|
|
|
3218
3243
|
title = `${name} (basic auth, paste base64 of user:password)`;
|
|
3219
3244
|
} else if (s.type === "oauth2" || s.type === "openIdConnect") {
|
|
3220
3245
|
apply = { headers: { authorization: `Bearer ${ref}` } };
|
|
3221
|
-
|
|
3246
|
+
const base = source && isUrl(source) ? source : void 0;
|
|
3247
|
+
oauth = s.type === "oauth2" ? codeFlow(s, base) : discovery(s, base);
|
|
3248
|
+
title = `${name} (OAuth, ${oauth ? "sign in or " : ""}paste an access token)`;
|
|
3222
3249
|
}
|
|
3223
3250
|
if (!apply) continue;
|
|
3224
|
-
out.auth[name] = { title, apply };
|
|
3251
|
+
out.auth[name] = { title, apply, ...oauth ? { oauth } : {} };
|
|
3225
3252
|
out.secrets[secret] = {};
|
|
3226
3253
|
}
|
|
3227
3254
|
const top = api.security?.flatMap(
|
|
@@ -3230,18 +3257,70 @@ function authFromSpec(api) {
|
|
|
3230
3257
|
out.defaultProfile = top?.find((n) => out.auth[n]) ?? Object.keys(out.auth)[0];
|
|
3231
3258
|
return out;
|
|
3232
3259
|
}
|
|
3233
|
-
|
|
3234
|
-
|
|
3235
|
-
|
|
3236
|
-
|
|
3237
|
-
|
|
3238
|
-
|
|
3260
|
+
var MAX_SERVERS = 20;
|
|
3261
|
+
var slug2 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 24).replace(/-+$/, "");
|
|
3262
|
+
function resolveServer(entry, source) {
|
|
3263
|
+
if (typeof entry?.url !== "string") return void 0;
|
|
3264
|
+
const url = entry.url.replace(/\{([^}]+)\}/g, (whole, key) => {
|
|
3265
|
+
const d = entry.variables?.[key]?.default;
|
|
3266
|
+
return d === void 0 || d === null ? whole : String(d);
|
|
3267
|
+
}).trim();
|
|
3268
|
+
if (/\{[^}]*\}/.test(url)) return void 0;
|
|
3269
|
+
if (isUrl(url)) return url.replace(/\/+$/, "");
|
|
3270
|
+
if (source) {
|
|
3271
|
+
try {
|
|
3272
|
+
return new URL(url, source).toString().replace(/\/+$/, "");
|
|
3273
|
+
} catch {
|
|
3274
|
+
}
|
|
3239
3275
|
}
|
|
3240
|
-
if (api.host)
|
|
3241
|
-
return `${api.schemes?.[0] ?? "https"}://${api.host}${api.basePath ?? ""}`.replace(/\/$/, "");
|
|
3242
|
-
if (isUrl(source)) return new URL(source).origin;
|
|
3243
3276
|
return void 0;
|
|
3244
3277
|
}
|
|
3278
|
+
function openApiServers(api, source) {
|
|
3279
|
+
const src = isUrl(source) ? source : void 0;
|
|
3280
|
+
const listed = Array.isArray(api.servers) ? api.servers : [];
|
|
3281
|
+
const servers = [];
|
|
3282
|
+
const describe = (e) => typeof e?.description === "string" && e.description.trim() !== "" ? { description: e.description.trim() } : {};
|
|
3283
|
+
if (listed.length === 0) {
|
|
3284
|
+
if (api.host)
|
|
3285
|
+
servers.push({
|
|
3286
|
+
name: "live",
|
|
3287
|
+
url: `${api.schemes?.[0] ?? "https"}://${api.host}${api.basePath ?? ""}`.replace(
|
|
3288
|
+
/\/+$/,
|
|
3289
|
+
""
|
|
3290
|
+
)
|
|
3291
|
+
});
|
|
3292
|
+
else if (src) servers.push({ name: "live", url: new URL(src).origin });
|
|
3293
|
+
return { servers, needsBaseUrl: false, omitted: 0 };
|
|
3294
|
+
}
|
|
3295
|
+
const first = resolveServer(listed[0], src);
|
|
3296
|
+
if (first) servers.push({ name: "live", url: first, ...describe(listed[0]) });
|
|
3297
|
+
const needsBaseUrl = !first && typeof listed[0]?.url === "string" && !/^https?:\/\//i.test(listed[0].url);
|
|
3298
|
+
const taken = /* @__PURE__ */ new Set(["mock", "live"]);
|
|
3299
|
+
const seen = new Set(servers.map((x2) => x2.url));
|
|
3300
|
+
let omitted = 0;
|
|
3301
|
+
for (const entry of listed.slice(1)) {
|
|
3302
|
+
const url = resolveServer(entry, src);
|
|
3303
|
+
if (!url || seen.has(url)) continue;
|
|
3304
|
+
if (servers.length >= MAX_SERVERS) {
|
|
3305
|
+
omitted++;
|
|
3306
|
+
continue;
|
|
3307
|
+
}
|
|
3308
|
+
seen.add(url);
|
|
3309
|
+
const host = (() => {
|
|
3310
|
+
try {
|
|
3311
|
+
return new URL(url).host;
|
|
3312
|
+
} catch {
|
|
3313
|
+
return "";
|
|
3314
|
+
}
|
|
3315
|
+
})();
|
|
3316
|
+
const base = slug2(describe(entry).description ?? "") || slug2(host) || "server";
|
|
3317
|
+
let name = base;
|
|
3318
|
+
for (let n = 2; taken.has(name); n++) name = `${base}-${n}`;
|
|
3319
|
+
taken.add(name);
|
|
3320
|
+
servers.push({ name, url, ...describe(entry) });
|
|
3321
|
+
}
|
|
3322
|
+
return { servers, needsBaseUrl, omitted };
|
|
3323
|
+
}
|
|
3245
3324
|
|
|
3246
3325
|
// ../vite/dist/index.js
|
|
3247
3326
|
import { existsSync as existsSync3, readdirSync } from "fs";
|
|
@@ -4602,66 +4681,22 @@ import { join as join22, relative as relative32, resolve as resolve3, sep as sep
|
|
|
4602
4681
|
import fg3 from "fast-glob";
|
|
4603
4682
|
import { readFile as readFile4 } from "fs/promises";
|
|
4604
4683
|
import { join as join32 } from "path";
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
for (const line2 of m[1].split(/\r?\n/)) {
|
|
4610
|
-
const i = line2.indexOf(":");
|
|
4611
|
-
if (i > 0)
|
|
4612
|
-
meta[line2.slice(0, i).trim()] = line2.slice(i + 1).trim().replace(/^["']|["']$/g, "");
|
|
4684
|
+
var OAuthError = class extends Error {
|
|
4685
|
+
constructor(message) {
|
|
4686
|
+
super(message);
|
|
4687
|
+
this.name = "OAuthError";
|
|
4613
4688
|
}
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
if (
|
|
4621
|
-
|
|
4622
|
-
const
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
if (flat && !NOT_AN_OPERATION.test(flat[2]))
|
|
4626
|
-
return { operation: `${flat[1]}/${flat[2]}` };
|
|
4627
|
-
return void 0;
|
|
4628
|
-
}
|
|
4629
|
-
async function scanDocs(root) {
|
|
4630
|
-
const files = await fg("**/*.md", {
|
|
4631
|
-
cwd: root,
|
|
4632
|
-
absolute: true,
|
|
4633
|
-
ignore: ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"]
|
|
4634
|
-
});
|
|
4635
|
-
const pages = [];
|
|
4636
|
-
const claimed = /* @__PURE__ */ new Map();
|
|
4637
|
-
for (const abs of files.sort()) {
|
|
4638
|
-
const { meta, body } = frontmatter(await readFile3(abs, "utf8"));
|
|
4639
|
-
const file = relative3(root, abs).split(sep2).join("/");
|
|
4640
|
-
const near = colocated(file);
|
|
4641
|
-
if (!near && !meta.operation && !meta.sequence && !meta.page) continue;
|
|
4642
|
-
const operation = meta.operation ?? near?.operation;
|
|
4643
|
-
if (operation) {
|
|
4644
|
-
const first = claimed.get(operation);
|
|
4645
|
-
if (first) {
|
|
4646
|
-
console.warn(
|
|
4647
|
-
`[specbook] ${file} documents ${operation}, which ${first} already does; ignored`
|
|
4648
|
-
);
|
|
4649
|
-
continue;
|
|
4650
|
-
}
|
|
4651
|
-
claimed.set(operation, file);
|
|
4652
|
-
}
|
|
4653
|
-
pages.push({
|
|
4654
|
-
file,
|
|
4655
|
-
operation,
|
|
4656
|
-
sequence: meta.sequence,
|
|
4657
|
-
sequenceFile: near?.sequenceFile,
|
|
4658
|
-
requestFile: near?.requestFile,
|
|
4659
|
-
replace: meta.replace === "true" || void 0,
|
|
4660
|
-
title: meta.title ?? meta.page,
|
|
4661
|
-
html: f.parse(body, { async: false })
|
|
4662
|
-
});
|
|
4663
|
-
}
|
|
4664
|
-
return pages;
|
|
4689
|
+
};
|
|
4690
|
+
function parseDiscovery(json3) {
|
|
4691
|
+
const d = json3 && typeof json3 === "object" ? json3 : {};
|
|
4692
|
+
const url = (v2) => typeof v2 === "string" && /^https?:\/\//i.test(v2) ? v2 : void 0;
|
|
4693
|
+
const authorizationUrl = url(d.authorization_endpoint);
|
|
4694
|
+
const tokenUrl = url(d.token_endpoint);
|
|
4695
|
+
if (!authorizationUrl || !tokenUrl)
|
|
4696
|
+
throw new OAuthError("The provider\u2019s discovery document has no authorization or token endpoint");
|
|
4697
|
+
const supported = Array.isArray(d.scopes_supported) ? d.scopes_supported.filter((x2) => typeof x2 === "string") : [];
|
|
4698
|
+
const names = ["openid", ...supported.filter((x2) => x2 !== "openid")];
|
|
4699
|
+
return { authorizationUrl, tokenUrl, scopes: Object.fromEntries(names.map((n) => [n, ""])) };
|
|
4665
4700
|
}
|
|
4666
4701
|
var DEFAULT_METHODS = ["GET", "HEAD", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"];
|
|
4667
4702
|
var DEFAULT_MAX_BODY = 25 * 1024 * 1024;
|
|
@@ -4949,6 +4984,107 @@ function parseDotenv2(text) {
|
|
|
4949
4984
|
}
|
|
4950
4985
|
return out;
|
|
4951
4986
|
}
|
|
4987
|
+
var FETCH_TIMEOUT_MS = 5e3;
|
|
4988
|
+
function createDiscovery(opts = {}) {
|
|
4989
|
+
const f2 = opts.fetch ?? fetch;
|
|
4990
|
+
const now = opts.now ?? Date.now;
|
|
4991
|
+
const retryAfter = opts.retryAfterMs ?? 3e4;
|
|
4992
|
+
const found = /* @__PURE__ */ new Map();
|
|
4993
|
+
const failedAt = /* @__PURE__ */ new Map();
|
|
4994
|
+
const inFlight = /* @__PURE__ */ new Map();
|
|
4995
|
+
async function look(url) {
|
|
4996
|
+
try {
|
|
4997
|
+
const res = await f2(url, {
|
|
4998
|
+
headers: { accept: "application/json" },
|
|
4999
|
+
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS)
|
|
5000
|
+
});
|
|
5001
|
+
if (!res.ok) throw new Error(`answered ${res.status}`);
|
|
5002
|
+
const origin = new URL(parseDiscovery(await res.json()).tokenUrl).origin;
|
|
5003
|
+
found.set(url, origin);
|
|
5004
|
+
failedAt.delete(url);
|
|
5005
|
+
return origin;
|
|
5006
|
+
} catch (e) {
|
|
5007
|
+
failedAt.set(url, now());
|
|
5008
|
+
opts.warn?.(
|
|
5009
|
+
`Could not read the OpenID Connect discovery document at ${url} (${e instanceof Error ? e.message : String(e)}). Sign in is unavailable until it can; pasting a token still works.`
|
|
5010
|
+
);
|
|
5011
|
+
return void 0;
|
|
5012
|
+
}
|
|
5013
|
+
}
|
|
5014
|
+
return function tokenOrigin(url) {
|
|
5015
|
+
const known = found.get(url);
|
|
5016
|
+
if (known) return Promise.resolve(known);
|
|
5017
|
+
const failed = failedAt.get(url);
|
|
5018
|
+
if (failed !== void 0 && now() - failed < retryAfter) return Promise.resolve(void 0);
|
|
5019
|
+
let pending = inFlight.get(url);
|
|
5020
|
+
if (!pending) {
|
|
5021
|
+
pending = look(url).finally(() => inFlight.delete(url));
|
|
5022
|
+
inFlight.set(url, pending);
|
|
5023
|
+
}
|
|
5024
|
+
return pending;
|
|
5025
|
+
};
|
|
5026
|
+
}
|
|
5027
|
+
function frontmatter(src) {
|
|
5028
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---\r?\n?/.exec(src);
|
|
5029
|
+
if (!m) return { meta: {}, body: src };
|
|
5030
|
+
const meta = {};
|
|
5031
|
+
for (const line2 of m[1].split(/\r?\n/)) {
|
|
5032
|
+
const i = line2.indexOf(":");
|
|
5033
|
+
if (i > 0)
|
|
5034
|
+
meta[line2.slice(0, i).trim()] = line2.slice(i + 1).trim().replace(/^["']|["']$/g, "");
|
|
5035
|
+
}
|
|
5036
|
+
return { meta, body: src.slice(m[0].length) };
|
|
5037
|
+
}
|
|
5038
|
+
var OP_FOLDER = /(?:^|\/)(?:requests|apis)\/([^/]+)\/([^/]+)\/(?:readme|index)\.md$/i;
|
|
5039
|
+
var OP_FLAT = /(?:^|\/)(?:requests|apis)\/([^/]+)\/docs\/([^/]+)\.md$/i;
|
|
5040
|
+
var NOT_AN_OPERATION = /^(readme|index)$/i;
|
|
5041
|
+
function colocated(file) {
|
|
5042
|
+
if (/\.seq\.md$/.test(file)) return { sequenceFile: file.replace(/\.md$/, ".ts") };
|
|
5043
|
+
if (/\.req\.md$/.test(file)) return { requestFile: file.replace(/\.md$/, ".ts") };
|
|
5044
|
+
const folder = OP_FOLDER.exec(file);
|
|
5045
|
+
if (folder && folder[2] !== "docs") return { operation: `${folder[1]}/${folder[2]}` };
|
|
5046
|
+
const flat = OP_FLAT.exec(file);
|
|
5047
|
+
if (flat && !NOT_AN_OPERATION.test(flat[2]))
|
|
5048
|
+
return { operation: `${flat[1]}/${flat[2]}` };
|
|
5049
|
+
return void 0;
|
|
5050
|
+
}
|
|
5051
|
+
async function scanDocs(root) {
|
|
5052
|
+
const files = await fg("**/*.md", {
|
|
5053
|
+
cwd: root,
|
|
5054
|
+
absolute: true,
|
|
5055
|
+
ignore: ["**/node_modules/**", "**/.generated/**", "**/dist/**", "**/.git/**"]
|
|
5056
|
+
});
|
|
5057
|
+
const pages = [];
|
|
5058
|
+
const claimed = /* @__PURE__ */ new Map();
|
|
5059
|
+
for (const abs of files.sort()) {
|
|
5060
|
+
const { meta, body } = frontmatter(await readFile3(abs, "utf8"));
|
|
5061
|
+
const file = relative3(root, abs).split(sep2).join("/");
|
|
5062
|
+
const near = colocated(file);
|
|
5063
|
+
if (!near && !meta.operation && !meta.sequence && !meta.page) continue;
|
|
5064
|
+
const operation = meta.operation ?? near?.operation;
|
|
5065
|
+
if (operation) {
|
|
5066
|
+
const first = claimed.get(operation);
|
|
5067
|
+
if (first) {
|
|
5068
|
+
console.warn(
|
|
5069
|
+
`[specbook] ${file} documents ${operation}, which ${first} already does; ignored`
|
|
5070
|
+
);
|
|
5071
|
+
continue;
|
|
5072
|
+
}
|
|
5073
|
+
claimed.set(operation, file);
|
|
5074
|
+
}
|
|
5075
|
+
pages.push({
|
|
5076
|
+
file,
|
|
5077
|
+
operation,
|
|
5078
|
+
sequence: meta.sequence,
|
|
5079
|
+
sequenceFile: near?.sequenceFile,
|
|
5080
|
+
requestFile: near?.requestFile,
|
|
5081
|
+
replace: meta.replace === "true" || void 0,
|
|
5082
|
+
title: meta.title ?? meta.page,
|
|
5083
|
+
html: f.parse(body, { async: false })
|
|
5084
|
+
});
|
|
5085
|
+
}
|
|
5086
|
+
return pages;
|
|
5087
|
+
}
|
|
4952
5088
|
var ENV_NAME = /^[A-Za-z0-9_-]+$/;
|
|
4953
5089
|
var VAR_NAME = /^[A-Za-z_][\w.-]*$/;
|
|
4954
5090
|
var environmentsDir = (root) => join3(root, ".specbook", "environments");
|
|
@@ -5106,6 +5242,15 @@ function safeProjectPath(root, relPath) {
|
|
|
5106
5242
|
function requestsDirFor(root) {
|
|
5107
5243
|
return existsSync2(join22(root, "apis")) && !existsSync2(join22(root, "requests")) ? "apis" : "requests";
|
|
5108
5244
|
}
|
|
5245
|
+
var AUTH_SERVICE_PREFIX = "auth:";
|
|
5246
|
+
var DISCOVERY_SERVICE_PREFIX = "discovery:";
|
|
5247
|
+
var decode = (s) => {
|
|
5248
|
+
try {
|
|
5249
|
+
return decodeURIComponent(s);
|
|
5250
|
+
} catch {
|
|
5251
|
+
return s;
|
|
5252
|
+
}
|
|
5253
|
+
};
|
|
5109
5254
|
var MOUNT = "/__specbook/proxy";
|
|
5110
5255
|
function readBody(req) {
|
|
5111
5256
|
return new Promise((res, rej) => {
|
|
@@ -5133,7 +5278,13 @@ async function handleProxy2(req, res, deps) {
|
|
|
5133
5278
|
const response = await handleProxy(request, {
|
|
5134
5279
|
mount: MOUNT,
|
|
5135
5280
|
policy: {
|
|
5136
|
-
upstream: (env, service) =>
|
|
5281
|
+
upstream: (env, service) => {
|
|
5282
|
+
if (service.startsWith(AUTH_SERVICE_PREFIX))
|
|
5283
|
+
return config.tokenOrigins?.[decode(service.slice(AUTH_SERVICE_PREFIX.length))];
|
|
5284
|
+
if (service.startsWith(DISCOVERY_SERVICE_PREFIX))
|
|
5285
|
+
return config.discoveryOrigins?.[decode(service.slice(DISCOVERY_SERVICE_PREFIX.length))];
|
|
5286
|
+
return config.services[service]?.baseUrl[env];
|
|
5287
|
+
},
|
|
5137
5288
|
// a developer's own machine: their local services are the point
|
|
5138
5289
|
allowPrivateHosts: true
|
|
5139
5290
|
},
|
|
@@ -5364,6 +5515,7 @@ function specbook(options) {
|
|
|
5364
5515
|
let notifyRegistryChanged = () => {
|
|
5365
5516
|
};
|
|
5366
5517
|
const memorySecrets = {};
|
|
5518
|
+
const tokenOriginOf = createDiscovery({ warn: (m) => server?.config.logger.warn(m) });
|
|
5367
5519
|
async function registryCode() {
|
|
5368
5520
|
const files = await scanProject(root);
|
|
5369
5521
|
const manifests = {};
|
|
@@ -5647,11 +5799,27 @@ export const loadConfig = ${cfg ? imp(cfg) : '() => Promise.resolve({ default: {
|
|
|
5647
5799
|
if (!server || !cfgFile) return { services: {}, secretNames: {} };
|
|
5648
5800
|
const mod = await server.ssrLoadModule(cfgFile);
|
|
5649
5801
|
const cfg = mod.default ?? {};
|
|
5802
|
+
const tokenOrigins = {};
|
|
5803
|
+
const discoveryOrigins = {};
|
|
5804
|
+
for (const [name, profile] of Object.entries(cfg.auth ?? {})) {
|
|
5805
|
+
try {
|
|
5806
|
+
const { tokenUrl, openIdConnectUrl } = profile.oauth ?? {};
|
|
5807
|
+
if (tokenUrl) tokenOrigins[name] = new URL(tokenUrl).origin;
|
|
5808
|
+
else if (openIdConnectUrl) {
|
|
5809
|
+
discoveryOrigins[name] = new URL(openIdConnectUrl).origin;
|
|
5810
|
+
const token = await tokenOriginOf(openIdConnectUrl);
|
|
5811
|
+
if (token) tokenOrigins[name] = token;
|
|
5812
|
+
}
|
|
5813
|
+
} catch {
|
|
5814
|
+
}
|
|
5815
|
+
}
|
|
5650
5816
|
return {
|
|
5651
5817
|
services: Object.fromEntries(
|
|
5652
5818
|
Object.entries(cfg.services ?? {}).map(([k, v2]) => [k, { baseUrl: v2.baseUrl ?? {} }])
|
|
5653
5819
|
),
|
|
5654
|
-
secretNames: cfg.secrets ?? {}
|
|
5820
|
+
secretNames: cfg.secrets ?? {},
|
|
5821
|
+
tokenOrigins,
|
|
5822
|
+
discoveryOrigins
|
|
5655
5823
|
};
|
|
5656
5824
|
}
|
|
5657
5825
|
return {
|
|
@@ -5896,7 +6064,7 @@ import { existsSync as existsSync4, statSync } from "fs";
|
|
|
5896
6064
|
import { join as join6, resolve as resolve4 } from "path";
|
|
5897
6065
|
|
|
5898
6066
|
// src/commands/metadata.ts
|
|
5899
|
-
import { readFile as readFile6
|
|
6067
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
5900
6068
|
import { join as join5 } from "path";
|
|
5901
6069
|
import fg5 from "fast-glob";
|
|
5902
6070
|
var isSequence = (v2) => v2?.kind === "sequence";
|
|
@@ -5933,7 +6101,7 @@ async function writeMetadata(root, outDir, loader) {
|
|
|
5933
6101
|
} catch {
|
|
5934
6102
|
}
|
|
5935
6103
|
}
|
|
5936
|
-
await
|
|
6104
|
+
await writeAtomically(
|
|
5937
6105
|
join5(outDir, "specbook.json"),
|
|
5938
6106
|
`${JSON.stringify({ services, sequences }, null, 2)}
|
|
5939
6107
|
`
|
|
@@ -6104,14 +6272,14 @@ async function generateCommand(project, loader, only) {
|
|
|
6104
6272
|
|
|
6105
6273
|
// src/commands/init.ts
|
|
6106
6274
|
import { existsSync as existsSync6 } from "fs";
|
|
6107
|
-
import { mkdir as mkdir4, readFile as readFile8, writeFile as
|
|
6275
|
+
import { mkdir as mkdir4, readFile as readFile8, writeFile as writeFile4 } from "fs/promises";
|
|
6108
6276
|
import { join as join8 } from "path";
|
|
6109
6277
|
async function initCommand(cwd) {
|
|
6110
6278
|
const dir = join8(cwd, ".specbook");
|
|
6111
6279
|
await mkdir4(dir, { recursive: true });
|
|
6112
6280
|
const cfg = join8(dir, "specbook.config.ts");
|
|
6113
6281
|
if (!existsSync6(cfg)) {
|
|
6114
|
-
await
|
|
6282
|
+
await writeFile4(
|
|
6115
6283
|
cfg,
|
|
6116
6284
|
`import { defineConfig } from '@specbook/core'
|
|
6117
6285
|
|
|
@@ -6129,11 +6297,11 @@ export default defineConfig({
|
|
|
6129
6297
|
const envDir = await ensureEnvironmentsDir(cwd);
|
|
6130
6298
|
const example = join8(envDir, "staging.secrets.env.example");
|
|
6131
6299
|
if (!existsSync6(example)) {
|
|
6132
|
-
await
|
|
6300
|
+
await writeFile4(
|
|
6133
6301
|
example,
|
|
6134
6302
|
"# Copy to staging.secrets.env (gitignored) and fill in. One KEY=value per line.\n# YOUR_API_KEY=\n"
|
|
6135
6303
|
);
|
|
6136
|
-
await
|
|
6304
|
+
await writeFile4(
|
|
6137
6305
|
join8(envDir, "staging.env"),
|
|
6138
6306
|
'# Non-secret variables for the "staging" environment (committed).\n# region=eu-west-1\n'
|
|
6139
6307
|
);
|
|
@@ -6144,7 +6312,7 @@ export default defineConfig({
|
|
|
6144
6312
|
pkg.imports = { ...pkg.imports ?? {}, "#api/*": "./.generated/*/index.ts" };
|
|
6145
6313
|
pkg.scripts = { ...pkg.scripts ?? {} };
|
|
6146
6314
|
if (!pkg.scripts.prepare) pkg.scripts.prepare = "specbook generate";
|
|
6147
|
-
await
|
|
6315
|
+
await writeFile4(pkgPath, `${JSON.stringify(pkg, null, 2)}
|
|
6148
6316
|
`);
|
|
6149
6317
|
console.log('\u2713 added the "#api/*" subpath import and a "prepare" script to package.json');
|
|
6150
6318
|
const gi = join8(cwd, ".gitignore");
|
|
@@ -6153,7 +6321,7 @@ export default defineConfig({
|
|
|
6153
6321
|
(entry) => !current.split("\n").some((l) => l.trim() === entry)
|
|
6154
6322
|
);
|
|
6155
6323
|
if (missing.length) {
|
|
6156
|
-
await
|
|
6324
|
+
await writeFile4(
|
|
6157
6325
|
gi,
|
|
6158
6326
|
`${current}${current.endsWith("\n") || !current ? "" : "\n"}${missing.join("\n")}
|
|
6159
6327
|
`
|
|
@@ -6165,7 +6333,7 @@ export default defineConfig({
|
|
|
6165
6333
|
// src/commands/preview.ts
|
|
6166
6334
|
import { spawn } from "child_process";
|
|
6167
6335
|
import { existsSync as existsSync7, readFileSync } from "fs";
|
|
6168
|
-
import { mkdir as mkdir5, mkdtemp, rm as rm2, symlink, writeFile as
|
|
6336
|
+
import { mkdir as mkdir5, mkdtemp, rm as rm2, symlink, writeFile as writeFile5 } from "fs/promises";
|
|
6169
6337
|
import { createRequire as createRequire3 } from "module";
|
|
6170
6338
|
import { tmpdir } from "os";
|
|
6171
6339
|
import { dirname as dirname4, extname, join as join10, resolve as resolve7 } from "path";
|
|
@@ -6203,6 +6371,14 @@ async function devCommand(project, port = 5173) {
|
|
|
6203
6371
|
});
|
|
6204
6372
|
await server.listen();
|
|
6205
6373
|
server.printUrls();
|
|
6374
|
+
const signIn = Object.entries(project.config.auth ?? {}).filter(([, p]) => p.oauth);
|
|
6375
|
+
const url = server.resolvedUrls?.local[0];
|
|
6376
|
+
if (signIn.length && url) {
|
|
6377
|
+
console.log(
|
|
6378
|
+
` OAuth redirect URI to register for ${signIn.map(([n]) => n).join(", ")}: ${url}
|
|
6379
|
+
(it follows the port; pass --port to keep it the same between runs)`
|
|
6380
|
+
);
|
|
6381
|
+
}
|
|
6206
6382
|
return server;
|
|
6207
6383
|
}
|
|
6208
6384
|
async function buildCommand(project, outDir) {
|
|
@@ -6247,7 +6423,7 @@ async function copyViewer(project, outDir, viewerDir = resolve6(uiRoot(), "dist-
|
|
|
6247
6423
|
|
|
6248
6424
|
// src/commands/preview.ts
|
|
6249
6425
|
var isUrl2 = (s) => /^https?:\/\//i.test(s);
|
|
6250
|
-
var
|
|
6426
|
+
var slug3 = (s) => s.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || "api";
|
|
6251
6427
|
async function link(root, name) {
|
|
6252
6428
|
try {
|
|
6253
6429
|
let dir = dirname4(createRequire3(import.meta.url).resolve(name));
|
|
@@ -6270,8 +6446,11 @@ async function previewCommand(source, o = {}) {
|
|
|
6270
6446
|
console.log(`Reading ${source} \u2026`);
|
|
6271
6447
|
const api = await SwaggerParser2.parse(spec);
|
|
6272
6448
|
const title = api.info?.title ?? "API";
|
|
6273
|
-
const service =
|
|
6274
|
-
const
|
|
6449
|
+
const service = slug3(title);
|
|
6450
|
+
const { servers } = openApiServers(api, source);
|
|
6451
|
+
const baseUrl = Object.fromEntries(servers.map((s) => [s.name, s.url]));
|
|
6452
|
+
const where = servers.length === 0 ? "" : servers.length === 1 && servers[0]?.name === "live" ? `, live calls go to ${servers[0].url}` : `
|
|
6453
|
+
Environments: ${servers.map((s) => `${s.name} \u2192 ${s.url}`).join(", ")}`;
|
|
6275
6454
|
const root = await mkdtemp(join10(tmpdir(), "specbook-preview-"));
|
|
6276
6455
|
const cleanup = () => rm2(root, { recursive: true, force: true }).catch(() => {
|
|
6277
6456
|
});
|
|
@@ -6284,9 +6463,9 @@ async function previewCommand(source, o = {}) {
|
|
|
6284
6463
|
const ext = /ya?ml/.test(extname(new URL(source).pathname)) ? ".yaml" : ".json";
|
|
6285
6464
|
specFile = join10(root, `openapi/api${ext}`);
|
|
6286
6465
|
await mkdir5(dirname4(specFile), { recursive: true });
|
|
6287
|
-
await
|
|
6466
|
+
await writeFile5(specFile, await res.text());
|
|
6288
6467
|
}
|
|
6289
|
-
const { auth, secrets, defaultProfile } = authFromSpec(api);
|
|
6468
|
+
const { auth, secrets, defaultProfile } = authFromSpec(api, source);
|
|
6290
6469
|
const config = {
|
|
6291
6470
|
project: service,
|
|
6292
6471
|
secrets,
|
|
@@ -6294,22 +6473,20 @@ async function previewCommand(source, o = {}) {
|
|
|
6294
6473
|
services: {
|
|
6295
6474
|
[service]: {
|
|
6296
6475
|
spec: specFile,
|
|
6297
|
-
baseUrl
|
|
6476
|
+
baseUrl,
|
|
6298
6477
|
...defaultProfile ? { auth: defaultProfile } : {}
|
|
6299
6478
|
}
|
|
6300
6479
|
}
|
|
6301
6480
|
};
|
|
6302
6481
|
const configFile = join10(root, ".specbook/specbook.config.ts");
|
|
6303
6482
|
await mkdir5(dirname4(configFile), { recursive: true });
|
|
6304
|
-
await
|
|
6483
|
+
await writeFile5(configFile, `export default ${JSON.stringify(config, null, 2)}
|
|
6305
6484
|
`);
|
|
6306
6485
|
await link(root, "@specbook/core");
|
|
6307
6486
|
await link(root, "@faker-js/faker");
|
|
6308
6487
|
console.log(`Generating "${title}" \u2026`);
|
|
6309
6488
|
const out = await generateToDisk({ spec: specFile, service, outDir: join10(root, ".generated") });
|
|
6310
|
-
console.log(
|
|
6311
|
-
`\u2713 ${out.operations.length} operations${baseUrl ? `, live calls go to ${baseUrl}` : ""}`
|
|
6312
|
-
);
|
|
6489
|
+
console.log(`\u2713 ${out.operations.length} operations${where}`);
|
|
6313
6490
|
const project = { root, configFile, config };
|
|
6314
6491
|
const server = await devCommand(project, o.port ?? 5173);
|
|
6315
6492
|
const url = server.resolvedUrls?.local[0];
|
|
@@ -6326,7 +6503,7 @@ async function previewCommand(source, o = {}) {
|
|
|
6326
6503
|
}
|
|
6327
6504
|
|
|
6328
6505
|
// src/commands/run.ts
|
|
6329
|
-
import { readFile as readFile11, writeFile as
|
|
6506
|
+
import { readFile as readFile11, writeFile as writeFile6 } from "fs/promises";
|
|
6330
6507
|
import { createRequire as createRequire4 } from "module";
|
|
6331
6508
|
import { join as join13, resolve as resolve10 } from "path";
|
|
6332
6509
|
import { pathToFileURL } from "url";
|
|
@@ -6538,7 +6715,7 @@ Set them as environment variables, or in .specbook/environments/${o.env}.secrets
|
|
|
6538
6715
|
const [kind, path] = spec.split(/:(.*)/s);
|
|
6539
6716
|
if (kind === "ndjson") continue;
|
|
6540
6717
|
const text = kind === "json" ? toJson(results) : kind === "junit" ? toJUnit(results) : kind === "github" ? formatGithub(results) : formatPretty(results);
|
|
6541
|
-
if (path) await
|
|
6718
|
+
if (path) await writeFile6(resolve10(path), `${text}
|
|
6542
6719
|
`);
|
|
6543
6720
|
else console.log(text);
|
|
6544
6721
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@specbook/cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Specbook command line: init, add, generate, dev, build, run, check, secrets and preview.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"specbook",
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"dependencies": {
|
|
38
38
|
"@apidevtools/swagger-parser": "10.1.1",
|
|
39
39
|
"@faker-js/faker": "9.3.0",
|
|
40
|
-
"@specbook/codegen": "0.
|
|
41
|
-
"@specbook/core": "0.
|
|
42
|
-
"@specbook/ui": "0.
|
|
43
|
-
"@specbook/vite": "0.
|
|
40
|
+
"@specbook/codegen": "0.2.0",
|
|
41
|
+
"@specbook/core": "0.2.0",
|
|
42
|
+
"@specbook/ui": "0.2.0",
|
|
43
|
+
"@specbook/vite": "0.2.0",
|
|
44
44
|
"fast-glob": "3.3.2",
|
|
45
45
|
"swagger2openapi": "7.0.8",
|
|
46
46
|
"vite": "8.3.4",
|