@izykitten/ce 0.0.3-dev.8 → 0.0.3-dev.9
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 +26 -3
- package/dist/ce.js +155 -11
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -52,7 +52,30 @@ ce bun publish # direct publication with Bun
|
|
|
52
52
|
ce bun --stage publish ./package.tgz # npm staging; no Bun executable is needed
|
|
53
53
|
```
|
|
54
54
|
|
|
55
|
-
Staging always runs [`npm stage publish`](https://docs.npmjs.com/cli/v12/commands/npm-stage), using npm's configuration and lifecycle scripts. It requires an npm version with that command (CI uses 12.2.1). Direct publication uses the requested client
|
|
55
|
+
Staging always runs [`npm stage publish`](https://docs.npmjs.com/cli/v12/commands/npm-stage), using npm's configuration and lifecycle scripts. It requires an npm version with that command (CI uses 12.2.1). Direct publication uses the requested client. Existing authentication remains in effect unless one of ce's authentication options is selected. Ce does not retry a failed publish through another mode. The CI workflow stages npmjs releases and publishes directly to Forgejo.
|
|
56
|
+
|
|
57
|
+
For npm token authentication, name the environment variable that holds the token and pass an explicit native registry:
|
|
58
|
+
|
|
59
|
+
```sh
|
|
60
|
+
ce npm --token-env=NODE_AUTH_TOKEN --stage publish ./package.tgz \
|
|
61
|
+
--registry=https://registry.npmjs.org/
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
Ce supplies a registry/path-scoped token through npm's child-process environment. It preserves existing npm configuration and writes no credential files. It checks authentication with `npm whoami` before publishing; `--no-check-auth` skips that probe, and `--check-auth` enables it again. The credentials are passed only to the npm backend, including `ce bun --stage`; direct Bun publishing continues to use Bun's authentication. Native configuration retains its normal precedence, including scoped registry mappings. Ce's `--dry-run` previews the command without reading the token, authenticating, or running lifecycle scripts.
|
|
65
|
+
|
|
66
|
+
Forgejo's integrated npm registry can use the same Actions OIDC exchange as Docker login:
|
|
67
|
+
|
|
68
|
+
```yaml
|
|
69
|
+
enable-openid-connect: true
|
|
70
|
+
env:
|
|
71
|
+
AUD: your-authorized-integration-audience
|
|
72
|
+
steps:
|
|
73
|
+
- run: >-
|
|
74
|
+
ce npm --forgejo-oidc publish ./package.tgz
|
|
75
|
+
--registry=${{ forgejo.server_url }}/api/packages/${{ forgejo.repository_owner }}/npm/
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
`--forgejo-oidc` reads `AUD`, `ACTIONS_ID_TOKEN_REQUEST_URL` and `ACTIONS_ID_TOKEN_REQUEST_TOKEN`, fetches a JWT through ce's existing token exchange, and scopes it to the selected npm registry. The registry and token endpoint must have the same HTTP(S) origin. Tokens expiring within five minutes are refused. The JWT is fetched immediately before authentication/publication; npm receives a fixed token for that invocation. No credentials are saved and no static-token fallback occurs. Forgejo has no npm staging or `whoami` endpoint, so this mode requires direct npm publication and skips the authentication probe by default. Use `--check-auth` only for a compatible endpoint. The two authentication modes are mutually exclusive; existing native `ce npm login` behavior is unchanged.
|
|
56
79
|
|
|
57
80
|
Without an explicit tag, ce reads the selected package's version and infers the first prerelease label: `1.2.3-dev.8` gives `dev`, `1.2.3-rc.1` gives `rc`. Stable versions receive no injected tag, preserving the client's normal defaults and npm's check against implicitly moving `latest` to an older version. Numeric labels, version-like labels and the prerelease label `latest` require an explicit `--tag`.
|
|
58
81
|
|
|
@@ -320,9 +343,9 @@ Compared with the Python version:
|
|
|
320
343
|
|
|
321
344
|
[The Forgejo build workflow](.forgejo/workflows/build.yml) builds, type-checks and tests PRs, pushes to `main` and `dev`, version tags (`v*`), and manual runs. Every successful build uploads the packed npm tarball as an artifact.
|
|
322
345
|
|
|
323
|
-
Version-tag pushes
|
|
346
|
+
Version-tag pushes publish that exact tarball to the repository owner's Forgejo npm registry with `ce npm --forgejo-oidc publish`, then stage it on npmjs with `ce npm --token-env=NODE_AUTH_TOKEN --stage publish`. The Git tag must match `v` followed by the version in `package.json`. Ce infers a prerelease's channel from the packed version; stable versions keep npm's native default and version checks. For a manual run, select **stage** to request both release uploads and optionally set **npm_tag** to override the dist-tag. Manual runs without **stage** build, check and upload the package artifact.
|
|
324
347
|
|
|
325
|
-
|
|
348
|
+
The job enables Forgejo OIDC. Its public `AUD` identifies the Authorized Integration whose backend permissions must allow publication to this repository's package registry. The Forgejo step requires no static registry token. For npmjs, use a granular `NPM_TOKEN` with read/write access to `@izykitten/ce`; a [stage-only token](https://docs.npmjs.com/about-access-tokens#about-stage-only-tokens) keeps final publication dependent on maintainer approval. Ce configures the token and verifies authentication without interactive `npm login` or temporary credential files. `NPM_TOKEN` is available only to the npmjs staging step. Neither release step runs on PRs or ordinary branch pushes.
|
|
326
349
|
|
|
327
350
|
CI runs `npm stage publish`, which [uploads a pending version without publishing it](https://docs.npmjs.com/cli/v12/commands/npm-stage). Review and approve the staged version in npm's dashboard with your own authentication. Staged and published versions share the same version namespace: bump `package.json` to an unused version before staging, and use the matching `v...` Git tag for an automatic release run. The workflow uses npm 12.2.1 for staging and never runs the approval command.
|
|
328
351
|
|
package/dist/ce.js
CHANGED
|
@@ -350,8 +350,8 @@ function envFlag(name, unset = false, allowed = "true or false") {
|
|
|
350
350
|
function shellQuote(arg) {
|
|
351
351
|
return /^[A-Za-z0-9_@%+=:,./-]+$/u.test(arg) ? arg : `'${arg.replaceAll("'", `'\\''`)}'`;
|
|
352
352
|
}
|
|
353
|
-
function childEnv() {
|
|
354
|
-
return Object.fromEntries(Object.entries(process.env).filter((entry) => entry[1] !== undefined));
|
|
353
|
+
function childEnv(overrides = {}) {
|
|
354
|
+
return Object.fromEntries(Object.entries({ ...process.env, ...overrides }).filter((entry) => entry[1] !== undefined));
|
|
355
355
|
}
|
|
356
356
|
function which(command) {
|
|
357
357
|
const candidates = command.includes("/") ? [command] : (process.env.PATH ?? "").split(path3.delimiter).filter(Boolean).map((dir) => path3.join(dir, command));
|
|
@@ -366,11 +366,11 @@ function which(command) {
|
|
|
366
366
|
}
|
|
367
367
|
var system = {
|
|
368
368
|
which,
|
|
369
|
-
run(argv, input) {
|
|
369
|
+
run(argv, input, env) {
|
|
370
370
|
const result = spawnSync(argv[0], argv.slice(1), {
|
|
371
371
|
input,
|
|
372
372
|
stdio: [input === undefined ? "ignore" : "pipe", "ignore", "pipe"],
|
|
373
|
-
env: childEnv(),
|
|
373
|
+
env: childEnv(env),
|
|
374
374
|
encoding: "utf8",
|
|
375
375
|
maxBuffer: Infinity
|
|
376
376
|
});
|
|
@@ -392,13 +392,13 @@ var system = {
|
|
|
392
392
|
sleepSync(ms) {
|
|
393
393
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
|
|
394
394
|
},
|
|
395
|
-
exec(file, argv) {
|
|
395
|
+
exec(file, argv, env) {
|
|
396
396
|
const resolved = system.which(file);
|
|
397
397
|
if (!resolved) {
|
|
398
398
|
logError(`ce: ${file} not found on PATH`);
|
|
399
399
|
return 1;
|
|
400
400
|
}
|
|
401
|
-
process.execve(resolved, argv, childEnv());
|
|
401
|
+
process.execve(resolved, argv, childEnv(env));
|
|
402
402
|
throw new Error(`execve returned for ${file}`);
|
|
403
403
|
}
|
|
404
404
|
};
|
|
@@ -2885,6 +2885,17 @@ function parseCeOptions(given, options, tool) {
|
|
|
2885
2885
|
use(arg, "publishing");
|
|
2886
2886
|
options.stage = true;
|
|
2887
2887
|
argv = argv.slice(1);
|
|
2888
|
+
} else if (arg === "--token-env" || arg?.startsWith("--token-env=")) {
|
|
2889
|
+
use("--token-env", "publishing");
|
|
2890
|
+
options.tokenEnv = value("--token-env");
|
|
2891
|
+
} else if (arg === "--forgejo-oidc") {
|
|
2892
|
+
use(arg, "publishing");
|
|
2893
|
+
options.forgejoOidc = true;
|
|
2894
|
+
argv = argv.slice(1);
|
|
2895
|
+
} else if (arg === "--check-auth" || arg === "--no-check-auth") {
|
|
2896
|
+
use(arg, "publishing");
|
|
2897
|
+
options.checkAuth = arg === "--check-auth";
|
|
2898
|
+
argv = argv.slice(1);
|
|
2888
2899
|
} else if (arg === "--auto-tag" || arg === "--no-auto-tag") {
|
|
2889
2900
|
use(arg, "publishing");
|
|
2890
2901
|
options.autoTag = arg === "--auto-tag";
|
|
@@ -2900,6 +2911,62 @@ function parseCeOptions(given, options, tool) {
|
|
|
2900
2911
|
// src/publish.ts
|
|
2901
2912
|
import fs6 from "node:fs";
|
|
2902
2913
|
import path5 from "node:path";
|
|
2914
|
+
|
|
2915
|
+
// src/npm-auth.ts
|
|
2916
|
+
function registryUrl(registry) {
|
|
2917
|
+
if (!registry)
|
|
2918
|
+
throw new CeError("ce: token authentication requires an explicit native --registry");
|
|
2919
|
+
let url;
|
|
2920
|
+
try {
|
|
2921
|
+
url = new URL(registry);
|
|
2922
|
+
if (!["http:", "https:"].includes(url.protocol) || !url.hostname || url.username || url.password || url.search || url.hash || /[\u0000-\u0020\\=]/u.test(registry))
|
|
2923
|
+
throw new Error("invalid registry");
|
|
2924
|
+
} catch {
|
|
2925
|
+
throw new CeError("ce: token authentication requires an absolute HTTP(S) registry URL without credentials, query, fragment, whitespace, backslashes or equals signs");
|
|
2926
|
+
}
|
|
2927
|
+
if (!url.pathname.endsWith("/"))
|
|
2928
|
+
url.pathname += "/";
|
|
2929
|
+
return url;
|
|
2930
|
+
}
|
|
2931
|
+
function credentials(url, token) {
|
|
2932
|
+
return {
|
|
2933
|
+
registry: url.href,
|
|
2934
|
+
env: { [`npm_config_//${url.host}${url.pathname}:_authToken`]: token },
|
|
2935
|
+
token
|
|
2936
|
+
};
|
|
2937
|
+
}
|
|
2938
|
+
function npmAuth(registry, tokenEnv, preview) {
|
|
2939
|
+
if (!/^[A-Za-z_][A-Za-z\d_]*$/u.test(tokenEnv))
|
|
2940
|
+
throw new CeError("ce: --token-env requires an environment variable name");
|
|
2941
|
+
const url = registryUrl(registry);
|
|
2942
|
+
const token = preview ? "" : process.env[tokenEnv];
|
|
2943
|
+
if (!preview && (!token || /[\s\p{Cc}]/u.test(token)))
|
|
2944
|
+
throw new CeError(`ce: ${tokenEnv} must contain a nonempty npm token without control characters`);
|
|
2945
|
+
return credentials(url, token ?? "");
|
|
2946
|
+
}
|
|
2947
|
+
async function forgejoNpmAuth(registry, preview) {
|
|
2948
|
+
const url = registryUrl(registry);
|
|
2949
|
+
const audience = process.env.AUD;
|
|
2950
|
+
const requestUrl = process.env.ACTIONS_ID_TOKEN_REQUEST_URL;
|
|
2951
|
+
const requestToken = process.env.ACTIONS_ID_TOKEN_REQUEST_TOKEN;
|
|
2952
|
+
if (!audience || !requestUrl || !preview && !requestToken)
|
|
2953
|
+
throw new CeError("ce: Forgejo npm OIDC requires AUD, ACTIONS_ID_TOKEN_REQUEST_URL and ACTIONS_ID_TOKEN_REQUEST_TOKEN; enable-openid-connect must be enabled for the job");
|
|
2954
|
+
try {
|
|
2955
|
+
const request = new URL(requestUrl);
|
|
2956
|
+
if (request.username || request.password || request.origin !== url.origin)
|
|
2957
|
+
throw new Error("different server");
|
|
2958
|
+
} catch {
|
|
2959
|
+
throw new CeError("ce: Forgejo npm registry and OIDC token request must use the same HTTP(S) origin without URL credentials");
|
|
2960
|
+
}
|
|
2961
|
+
if (preview)
|
|
2962
|
+
return credentials(url, "");
|
|
2963
|
+
const token = await forgejoOidcJwt(audience, requestUrl, requestToken);
|
|
2964
|
+
if (needsRefresh(jwtExpiry(token)))
|
|
2965
|
+
throw new CeError("ce: Forgejo OIDC token expires within the five-minute window; refusing to publish");
|
|
2966
|
+
return credentials(url, token);
|
|
2967
|
+
}
|
|
2968
|
+
|
|
2969
|
+
// src/publish.ts
|
|
2903
2970
|
function isPackageManager(value) {
|
|
2904
2971
|
return value === "npm" || value === "bun";
|
|
2905
2972
|
}
|
|
@@ -2934,7 +3001,8 @@ var VALUES = new Set([
|
|
|
2934
3001
|
"--omit",
|
|
2935
3002
|
"--cpu",
|
|
2936
3003
|
"--os",
|
|
2937
|
-
"--libc"
|
|
3004
|
+
"--libc",
|
|
3005
|
+
"--token-env"
|
|
2938
3006
|
]);
|
|
2939
3007
|
var SWITCHES = new Set([
|
|
2940
3008
|
"--dry-run",
|
|
@@ -2978,7 +3046,28 @@ var WORKSPACES = new Set([
|
|
|
2978
3046
|
"--recursive",
|
|
2979
3047
|
"-r"
|
|
2980
3048
|
]);
|
|
2981
|
-
var CE_FLAGS = new Set([
|
|
3049
|
+
var CE_FLAGS = new Set([
|
|
3050
|
+
"--stage",
|
|
3051
|
+
"--auto-tag",
|
|
3052
|
+
"--no-auto-tag",
|
|
3053
|
+
"--token-env",
|
|
3054
|
+
"--check-auth",
|
|
3055
|
+
"--no-check-auth",
|
|
3056
|
+
"--forgejo-oidc"
|
|
3057
|
+
]);
|
|
3058
|
+
var AUTH_CONFIG = new Set([
|
|
3059
|
+
"--userconfig",
|
|
3060
|
+
"--globalconfig",
|
|
3061
|
+
"--cafile",
|
|
3062
|
+
"--ca",
|
|
3063
|
+
"--cache",
|
|
3064
|
+
"--prefix",
|
|
3065
|
+
"--loglevel",
|
|
3066
|
+
"--logs-dir",
|
|
3067
|
+
"--use-system-ca",
|
|
3068
|
+
"--use-openssl-ca",
|
|
3069
|
+
"--use-bundled-ca"
|
|
3070
|
+
]);
|
|
2982
3071
|
var PORTABLE_STAGE = new Set([
|
|
2983
3072
|
"--tag",
|
|
2984
3073
|
"--access",
|
|
@@ -3112,13 +3201,14 @@ function inferTag(data) {
|
|
|
3112
3201
|
throw new CeError(`ce: prerelease ${JSON.stringify(data.version)} needs an explicit --tag; its first label is ambiguous or promotes latest`);
|
|
3113
3202
|
return channel;
|
|
3114
3203
|
}
|
|
3115
|
-
function runPackageManager(manager, given, options = {}) {
|
|
3204
|
+
async function runPackageManager(manager, given, options = {}) {
|
|
3116
3205
|
const dryRun = options.dryRun ?? envFlag("CE_DRY_RUN");
|
|
3117
3206
|
const command = invocation(manager, given);
|
|
3118
3207
|
if (options.stage && !command)
|
|
3119
3208
|
throw new CeError("ce: --stage requires npm publish or bun publish");
|
|
3120
3209
|
const staged = options.stage || command?.staged;
|
|
3121
3210
|
const client = staged ? "npm" : manager;
|
|
3211
|
+
const help = given.some((arg) => ["--help", "-h", "--version"].includes(arg));
|
|
3122
3212
|
let args = given;
|
|
3123
3213
|
if (command) {
|
|
3124
3214
|
for (let i = 0;i < given.length; ) {
|
|
@@ -3141,7 +3231,6 @@ function runPackageManager(manager, given, options = {}) {
|
|
|
3141
3231
|
i = arg.startsWith("-") ? nextIndex(given, i) : i + 1;
|
|
3142
3232
|
}
|
|
3143
3233
|
}
|
|
3144
|
-
const help = given.some((arg) => ["--help", "-h", "--version"].includes(arg));
|
|
3145
3234
|
if ((options.autoTag ?? true) && !help && !explicitTag(given) && !(client === "npm" && (process.env.npm_config_tag || process.env.NPM_CONFIG_TAG))) {
|
|
3146
3235
|
const tag = inferTag(manifest(manager, prefix, body));
|
|
3147
3236
|
if (tag)
|
|
@@ -3153,6 +3242,38 @@ function runPackageManager(manager, given, options = {}) {
|
|
|
3153
3242
|
...body
|
|
3154
3243
|
];
|
|
3155
3244
|
}
|
|
3245
|
+
let auth;
|
|
3246
|
+
const authConfig = [];
|
|
3247
|
+
if (!help && (options.tokenEnv !== undefined || options.checkAuth !== undefined || options.forgejoOidc)) {
|
|
3248
|
+
if (!command || client !== "npm")
|
|
3249
|
+
throw new CeError("ce: token authentication requires npm publish or bun --stage publish");
|
|
3250
|
+
if (options.tokenEnv !== undefined && options.forgejoOidc)
|
|
3251
|
+
throw new CeError("ce: choose --token-env or --forgejo-oidc, not both");
|
|
3252
|
+
if (options.tokenEnv === undefined && !options.forgejoOidc)
|
|
3253
|
+
throw new CeError("ce: --check-auth and --no-check-auth require --token-env or --forgejo-oidc");
|
|
3254
|
+
if (options.forgejoOidc && staged)
|
|
3255
|
+
throw new CeError("ce: the Forgejo npm registry does not support staged publishing; use direct publish");
|
|
3256
|
+
let registry;
|
|
3257
|
+
for (let i = 0;i < args.length; ) {
|
|
3258
|
+
const arg = args[i];
|
|
3259
|
+
if (arg === "--")
|
|
3260
|
+
break;
|
|
3261
|
+
const end = arg.startsWith("-") ? nextIndex(args, i) : i + 1;
|
|
3262
|
+
if (option(arg) === "--registry")
|
|
3263
|
+
registry = {
|
|
3264
|
+
index: i,
|
|
3265
|
+
joined: arg.includes("="),
|
|
3266
|
+
value: arg.includes("=") ? arg.slice(arg.indexOf("=") + 1) : args[i + 1]
|
|
3267
|
+
};
|
|
3268
|
+
if (AUTH_CONFIG.has(option(arg)))
|
|
3269
|
+
authConfig.push(...args.slice(i, end));
|
|
3270
|
+
i = end;
|
|
3271
|
+
}
|
|
3272
|
+
auth = options.forgejoOidc ? await forgejoNpmAuth(registry?.value, dryRun) : npmAuth(registry?.value, options.tokenEnv, dryRun);
|
|
3273
|
+
args = [...args];
|
|
3274
|
+
if (registry)
|
|
3275
|
+
args[registry.joined ? registry.index : registry.index + 1] = registry.joined ? `--registry=${auth.registry}` : auth.registry;
|
|
3276
|
+
}
|
|
3156
3277
|
if (dryRun) {
|
|
3157
3278
|
if (!system.which(client))
|
|
3158
3279
|
logError(`ce: warning: ${client} is not on PATH, so running this here would fail`);
|
|
@@ -3160,7 +3281,19 @@ function runPackageManager(manager, given, options = {}) {
|
|
|
3160
3281
|
`);
|
|
3161
3282
|
return 0;
|
|
3162
3283
|
}
|
|
3163
|
-
|
|
3284
|
+
if (auth && (options.checkAuth ?? !options.forgejoOidc)) {
|
|
3285
|
+
if (!system.which("npm")) {
|
|
3286
|
+
logError("ce: npm not found on PATH");
|
|
3287
|
+
return 1;
|
|
3288
|
+
}
|
|
3289
|
+
const result = system.run(["npm", "whoami", ...authConfig, `--registry=${auth.registry}`], undefined, auth.env);
|
|
3290
|
+
if (result.status !== 0) {
|
|
3291
|
+
logError(`ce: npm authentication failed for ${auth.registry}`);
|
|
3292
|
+
io.stderr(result.stderr.replaceAll(auth.token, "[REDACTED]"));
|
|
3293
|
+
return result.status;
|
|
3294
|
+
}
|
|
3295
|
+
}
|
|
3296
|
+
return auth ? system.exec(client, [client, ...args], auth.env) : system.exec(client, [client, ...args]);
|
|
3164
3297
|
}
|
|
3165
3298
|
|
|
3166
3299
|
// src/cli.ts
|
|
@@ -3213,6 +3346,15 @@ Publishing options (npm and bun):
|
|
|
3213
3346
|
Explicit --tag and publishConfig.tag take precedence.
|
|
3214
3347
|
With npm, npm_config_tag/NPM_CONFIG_TAG also take precedence.
|
|
3215
3348
|
Stable versions retain the native tag default and checks.
|
|
3349
|
+
--token-env=NAME Configure npm token authentication from an environment
|
|
3350
|
+
variable for this invocation. Requires native --registry.
|
|
3351
|
+
Supports npm and Bun staging; saves no credentials.
|
|
3352
|
+
--forgejo-oidc Fetch a Forgejo JWT using AUD and the Actions OIDC request
|
|
3353
|
+
variables. Requires npm direct publish and --registry
|
|
3354
|
+
on the same server; saves no credentials. No staging.
|
|
3355
|
+
--no-check-auth Skip the npm whoami check enabled by --token-env.
|
|
3356
|
+
--check-auth Enable that check (off by default for Forgejo).
|
|
3357
|
+
Requires --token-env or --forgejo-oidc.
|
|
3216
3358
|
Examples:
|
|
3217
3359
|
ce npm --stage publish ./package.tgz
|
|
3218
3360
|
ce bun publish --tag next
|
|
@@ -3317,6 +3459,8 @@ async function main(argv) {
|
|
|
3317
3459
|
throw new CeError("ce: login takes no native arguments; use ce <engine> login, or place ce options before login");
|
|
3318
3460
|
if (options.used.has("--stage"))
|
|
3319
3461
|
throw new CeError("ce: --stage requires npm publish or bun publish");
|
|
3462
|
+
if (options.tokenEnv !== undefined || options.checkAuth !== undefined || options.forgejoOidc)
|
|
3463
|
+
throw new CeError("ce: token authentication requires npm publish or bun --stage publish");
|
|
3320
3464
|
return login(undefined, options.dryRun ?? envFlag("CE_DRY_RUN"));
|
|
3321
3465
|
}
|
|
3322
3466
|
if (command === "credential-helper")
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@izykitten/ce",
|
|
3
|
-
"version": "0.0.3-dev.
|
|
3
|
+
"version": "0.0.3-dev.9",
|
|
4
4
|
"description": "CI wrapper for container builds and npm/Bun publishing, with tag/cache rewriting, registry login, and a Forgejo OIDC Docker credential helper",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "BSD-3-Clause",
|