@hyper.authio/cli 0.0.0-stage → 0.11.1
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 +71 -2
- package/dist/cli.js +498 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.cjs +515 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +264 -0
- package/dist/index.d.ts +264 -0
- package/dist/index.js +490 -0
- package/dist/index.js.map +1 -0
- package/package.json +53 -4
package/dist/index.cjs
ADDED
|
@@ -0,0 +1,515 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var child_process = require('child_process');
|
|
4
|
+
var readline = require('readline');
|
|
5
|
+
var fs = require('fs');
|
|
6
|
+
var path = require('path');
|
|
7
|
+
var crypto = require('crypto');
|
|
8
|
+
|
|
9
|
+
// src/errors.ts
|
|
10
|
+
var AuthioCliError = class extends Error {
|
|
11
|
+
/** What went wrong, stable across versions. */
|
|
12
|
+
code;
|
|
13
|
+
/** The process exit code the CLI ends with. */
|
|
14
|
+
exitCode;
|
|
15
|
+
constructor(code, message, options) {
|
|
16
|
+
super(message, options);
|
|
17
|
+
this.name = "AuthioCliError";
|
|
18
|
+
this.code = code;
|
|
19
|
+
this.exitCode = options?.exitCode ?? 1;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/args.ts
|
|
24
|
+
var DEFAULT_API = "https://authio.api.hyperio.tech";
|
|
25
|
+
var VALUE_OPTIONS = /* @__PURE__ */ new Set(["--ticket", "--verification", "--api", "--cwd"]);
|
|
26
|
+
var FLAG_OPTIONS = /* @__PURE__ */ new Set(["--dry-run", "--yes", "-y", "--wait", "--help", "-h", "--version", "-v"]);
|
|
27
|
+
function parseArguments(argv) {
|
|
28
|
+
const values = /* @__PURE__ */ new Map();
|
|
29
|
+
const flags = /* @__PURE__ */ new Set();
|
|
30
|
+
const positional = [];
|
|
31
|
+
for (let index = 0; index < argv.length; index++) {
|
|
32
|
+
const current = argv[index];
|
|
33
|
+
if (!current.startsWith("-")) {
|
|
34
|
+
positional.push(current);
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
const [name, inline] = current.includes("=") ? [current.slice(0, current.indexOf("=")), current.slice(current.indexOf("=") + 1)] : [current, void 0];
|
|
38
|
+
if (VALUE_OPTIONS.has(name)) {
|
|
39
|
+
const value = inline ?? argv[++index];
|
|
40
|
+
if (value === void 0 || value.startsWith("--")) throw new AuthioCliError("invalid_arguments", `${name} needs a value.`);
|
|
41
|
+
values.set(name, value);
|
|
42
|
+
} else if (FLAG_OPTIONS.has(name) && inline === void 0) {
|
|
43
|
+
flags.add(name);
|
|
44
|
+
} else {
|
|
45
|
+
throw new AuthioCliError("invalid_arguments", `Unknown option ${current}. Run "authio --help" to see the options.`);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
const named = positional[0];
|
|
49
|
+
const command = flags.has("--help") || flags.has("-h") ? "help" : flags.has("--version") || flags.has("-v") ? "version" : named === void 0 ? "help" : named;
|
|
50
|
+
if (command !== "init" && command !== "verify" && command !== "help" && command !== "version")
|
|
51
|
+
throw new AuthioCliError("invalid_arguments", `Unknown command "${command}". Use "init" or "verify".`);
|
|
52
|
+
return {
|
|
53
|
+
command,
|
|
54
|
+
ticket: values.get("--ticket"),
|
|
55
|
+
verification: values.get("--verification"),
|
|
56
|
+
api: values.get("--api"),
|
|
57
|
+
cwd: values.get("--cwd"),
|
|
58
|
+
dryRun: flags.has("--dry-run"),
|
|
59
|
+
yes: flags.has("--yes") || flags.has("-y"),
|
|
60
|
+
wait: flags.has("--wait")
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function apiAddress(api) {
|
|
64
|
+
const address = (api ?? DEFAULT_API).trim().replace(/\/+$/, "");
|
|
65
|
+
if (!/^https?:\/\/[^/\s]+/i.test(address)) throw new AuthioCliError("invalid_arguments", `--api must be an http or https address, not "${api}".`);
|
|
66
|
+
return address;
|
|
67
|
+
}
|
|
68
|
+
var HELP = `Authio CLI
|
|
69
|
+
|
|
70
|
+
Usage
|
|
71
|
+
npx "@hyper.authio/cli" init --ticket ait_... Apply a client's integration plan to this project
|
|
72
|
+
npx "@hyper.authio/cli" verify [--wait] Check whether the first sign-in has arrived
|
|
73
|
+
|
|
74
|
+
Options
|
|
75
|
+
--ticket <code> The one-time code from the client's guide in the console (or AUTHIO_TICKET)
|
|
76
|
+
--api <url> The Authio API address (default ${DEFAULT_API})
|
|
77
|
+
--cwd <dir> The project folder (default: the current folder)
|
|
78
|
+
--dry-run Show the plan without writing anything (the code is still used up)
|
|
79
|
+
--yes, -y Answer yes to every question; required without an interactive terminal
|
|
80
|
+
--verification <code> The verification code (default: the one init saved in .authio/verification)
|
|
81
|
+
--wait With verify: keep checking until the first sign-in arrives
|
|
82
|
+
|
|
83
|
+
Keep the package in double quotes: Windows PowerShell reads an unquoted @ as splatting and refuses the line.
|
|
84
|
+
The CLI never overwrites a file, never commits and never sends your code anywhere.`;
|
|
85
|
+
function ask(question, hidden) {
|
|
86
|
+
const input = process.stdin;
|
|
87
|
+
const output = process.stdout;
|
|
88
|
+
if (!hidden || !input.isTTY) {
|
|
89
|
+
const lines = readline.createInterface({ input, output });
|
|
90
|
+
return new Promise(
|
|
91
|
+
(resolve2) => lines.question(question, (answer) => {
|
|
92
|
+
lines.close();
|
|
93
|
+
resolve2(answer);
|
|
94
|
+
})
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
return new Promise((resolve2, reject) => {
|
|
98
|
+
let value = "";
|
|
99
|
+
output.write(question);
|
|
100
|
+
input.setRawMode(true);
|
|
101
|
+
input.resume();
|
|
102
|
+
input.setEncoding("utf8");
|
|
103
|
+
const finish = () => {
|
|
104
|
+
input.setRawMode(false);
|
|
105
|
+
input.pause();
|
|
106
|
+
input.removeListener("data", onData);
|
|
107
|
+
output.write("\n");
|
|
108
|
+
};
|
|
109
|
+
const onData = (chunk) => {
|
|
110
|
+
for (const character of chunk) {
|
|
111
|
+
if (character === "\r" || character === "\n") {
|
|
112
|
+
finish();
|
|
113
|
+
resolve2(value);
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (character === "") {
|
|
117
|
+
finish();
|
|
118
|
+
reject(new Error("Cancelled."));
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (character === "\x7F" || character === "\b") value = value.slice(0, -1);
|
|
122
|
+
else if (character >= " ") value += character;
|
|
123
|
+
}
|
|
124
|
+
};
|
|
125
|
+
input.on("data", onData);
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
function windowsCommandLine(command, args) {
|
|
129
|
+
return [command, ...args.map((arg) => `"${arg.replace(/"/g, '""')}"`)].join(" ");
|
|
130
|
+
}
|
|
131
|
+
function createTerminalIo() {
|
|
132
|
+
return {
|
|
133
|
+
fetch: globalThis.fetch.bind(globalThis),
|
|
134
|
+
print: (line) => process.stdout.write(`${line}
|
|
135
|
+
`),
|
|
136
|
+
confirm: async (question, defaultYes) => {
|
|
137
|
+
const answer = (await ask(`${question} ${defaultYes ? "[Y/n]" : "[y/N]"} `, false)).trim().toLowerCase();
|
|
138
|
+
return answer === "" ? defaultYes : answer === "y" || answer === "yes";
|
|
139
|
+
},
|
|
140
|
+
promptSecret: (question) => ask(question, true),
|
|
141
|
+
run: (command, args, cwd) => new Promise((resolve2) => {
|
|
142
|
+
const child = process.platform === "win32" ? child_process.spawn(windowsCommandLine(command, args), { cwd, stdio: "inherit", shell: true }) : child_process.spawn(command, [...args], { cwd, stdio: "inherit" });
|
|
143
|
+
child.on("error", () => resolve2(127));
|
|
144
|
+
child.on("close", (code) => resolve2(code ?? 1));
|
|
145
|
+
}),
|
|
146
|
+
isInteractive: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
147
|
+
now: () => Date.now(),
|
|
148
|
+
sleep: (milliseconds) => new Promise((resolve2) => setTimeout(resolve2, milliseconds))
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
var isDirectory = (path) => fs.existsSync(path) && fs.statSync(path).isDirectory();
|
|
152
|
+
function majorOf(range) {
|
|
153
|
+
const match = /(\d+)/.exec(range ?? "");
|
|
154
|
+
return match ? Number(match[1]) : void 0;
|
|
155
|
+
}
|
|
156
|
+
function detectProject(root) {
|
|
157
|
+
const manifestPath = path.join(root, "package.json");
|
|
158
|
+
if (!fs.existsSync(manifestPath))
|
|
159
|
+
throw new AuthioCliError("project_not_found", `No package.json in ${root}. Run the command in your Next.js project folder, or pass --cwd.`);
|
|
160
|
+
let manifest;
|
|
161
|
+
try {
|
|
162
|
+
manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8"));
|
|
163
|
+
} catch (cause) {
|
|
164
|
+
throw new AuthioCliError("project_not_found", `${manifestPath} could not be read as JSON.`, { cause });
|
|
165
|
+
}
|
|
166
|
+
const dependencies = { ...manifest.devDependencies, ...manifest.dependencies };
|
|
167
|
+
const nextMajor = majorOf(dependencies.next);
|
|
168
|
+
if (nextMajor === void 0)
|
|
169
|
+
throw new AuthioCliError("stack_not_detected", "This project does not depend on next. The plan this CLI applies is for Next.js with the App Router and Auth.js v5; for another stack, follow the client's guide in the console.");
|
|
170
|
+
const nextAuthMajor = majorOf(dependencies["next-auth"]);
|
|
171
|
+
if (nextAuthMajor !== void 0 && nextAuthMajor < 5)
|
|
172
|
+
throw new AuthioCliError("nextauth_v4_unsupported", `This project uses next-auth ${dependencies["next-auth"]}. The plan is written for Auth.js v5; follow the NextAuth v4 recipe in the client's guide instead.`);
|
|
173
|
+
const srcDir = isDirectory(path.join(root, "src", "app"));
|
|
174
|
+
const appDir = srcDir || isDirectory(path.join(root, "app"));
|
|
175
|
+
if (!appDir) {
|
|
176
|
+
const pages = isDirectory(path.join(root, "pages")) || isDirectory(path.join(root, "src", "pages"));
|
|
177
|
+
throw new AuthioCliError(
|
|
178
|
+
pages ? "pages_router_unsupported" : "stack_not_detected",
|
|
179
|
+
pages ? "This project uses the pages router. The plan is written for the App Router; follow the client's guide in the console instead." : "No app/ or src/app/ folder was found. Create the App Router folder first, or run the command in the app's folder."
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
const packageManager = fs.existsSync(path.join(root, "pnpm-lock.yaml")) ? "pnpm" : fs.existsSync(path.join(root, "yarn.lock")) ? "yarn" : fs.existsSync(path.join(root, "bun.lockb")) || fs.existsSync(path.join(root, "bun.lock")) ? "bun" : "npm";
|
|
183
|
+
const port = /(?:^|\s)(?:-p|--port)(?:\s+|=)(\d{2,5})(?:\s|$)/.exec(manifest.scripts?.dev ?? "");
|
|
184
|
+
return { root, nextMajor, srcDir, packageManager, devPort: port ? Number(port[1]) : 3e3 };
|
|
185
|
+
}
|
|
186
|
+
var SUPPORTED_PLAN_VERSIONS = [1];
|
|
187
|
+
function normalizePlan(plan) {
|
|
188
|
+
const lower = (value, fallback) => value ? String(value).toLowerCase() : fallback;
|
|
189
|
+
return {
|
|
190
|
+
...plan,
|
|
191
|
+
packages: plan.packages ?? [],
|
|
192
|
+
redirectUris: plan.redirectUris ?? [],
|
|
193
|
+
files: (plan.files ?? []).map((file) => ({ ...file, root: lower(file.root, "project"), content: file.content ?? "" })),
|
|
194
|
+
environment: (plan.environment ?? []).map((variable) => ({
|
|
195
|
+
...variable,
|
|
196
|
+
source: lower(variable.source, "secret")
|
|
197
|
+
}))
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
function assertPlanVersion(plan) {
|
|
201
|
+
if (!SUPPORTED_PLAN_VERSIONS.includes(plan.planVersion))
|
|
202
|
+
throw new AuthioCliError(
|
|
203
|
+
"plan_version_unsupported",
|
|
204
|
+
`This plan is version ${plan.planVersion}, which this CLI does not know. Run npx "@hyper.authio/cli@latest" with a new code.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
function placePath(project, file) {
|
|
208
|
+
const relative = path.normalize(file.path).replace(/\\/g, "/");
|
|
209
|
+
if (!relative || path.isAbsolute(file.path) || /^[a-zA-Z]:/.test(file.path) || relative.split("/").includes(".."))
|
|
210
|
+
throw new AuthioCliError("plan_path_invalid", `The plan names a path outside the project ("${file.path}"); nothing was written.`);
|
|
211
|
+
return file.root === "source" && project.srcDir ? `src/${relative}` : relative;
|
|
212
|
+
}
|
|
213
|
+
function readEnvKeys(path) {
|
|
214
|
+
if (!fs.existsSync(path)) return /* @__PURE__ */ new Set();
|
|
215
|
+
const keys = /* @__PURE__ */ new Set();
|
|
216
|
+
for (const line of fs.readFileSync(path, "utf8").split(/\r?\n/)) {
|
|
217
|
+
const match = /^\s*(?:export\s+)?([A-Za-z_][A-Za-z0-9_]*)\s*=/.exec(line);
|
|
218
|
+
if (match) keys.add(match[1]);
|
|
219
|
+
}
|
|
220
|
+
return keys;
|
|
221
|
+
}
|
|
222
|
+
var LOCAL_ORIGIN_VARIABLE = "AUTH_URL";
|
|
223
|
+
var LOCAL_ORIGIN_COMMENT = "Where this app runs on this machine; set the public address where it is deployed.";
|
|
224
|
+
function localOrigin(project) {
|
|
225
|
+
return `http://localhost:${project.devPort}`;
|
|
226
|
+
}
|
|
227
|
+
function planChanges(project, plan) {
|
|
228
|
+
const files = plan.files.map((file) => {
|
|
229
|
+
const path$1 = placePath(project, file);
|
|
230
|
+
const target = path.join(project.root, ...path$1.split("/"));
|
|
231
|
+
if (!target.startsWith(project.root + path.sep) && target !== project.root)
|
|
232
|
+
throw new AuthioCliError("plan_path_invalid", `The plan names a path outside the project ("${file.path}"); nothing was written.`);
|
|
233
|
+
return { path: path$1, target, content: file.content, action: fs.existsSync(target) ? "merge" : "create" };
|
|
234
|
+
});
|
|
235
|
+
const existing = readEnvKeys(path.join(project.root, ".env.local"));
|
|
236
|
+
const localRedirect = `${localOrigin(project)}${plan.callbackPath}`;
|
|
237
|
+
return {
|
|
238
|
+
files,
|
|
239
|
+
environment: plan.environment.filter((variable) => !existing.has(variable.name)).map((variable) => variable.name === LOCAL_ORIGIN_VARIABLE && variable.source === "plan" ? { ...variable, value: localOrigin(project), comment: LOCAL_ORIGIN_COMMENT } : variable),
|
|
240
|
+
keptEnvironment: plan.environment.filter((variable) => existing.has(variable.name)).map((variable) => variable.name),
|
|
241
|
+
localRedirect,
|
|
242
|
+
redirectRegistered: plan.redirectUris.some((uri) => uri.replace(/\/+$/, "") === localRedirect)
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
var IGNORED_PATHS = [".env.local", ".authio/"];
|
|
246
|
+
function patternCovers(pattern, path) {
|
|
247
|
+
const trimmed = pattern.trim();
|
|
248
|
+
if (!trimmed || trimmed.startsWith("#") || trimmed.startsWith("!")) return false;
|
|
249
|
+
const bare = trimmed.replace(/^\//, "");
|
|
250
|
+
const wantsDirectory = path.endsWith("/");
|
|
251
|
+
const name = path.replace(/\/$/, "");
|
|
252
|
+
if (bare.endsWith("/") && !wantsDirectory) return false;
|
|
253
|
+
const glob = bare.replace(/\/$/, "");
|
|
254
|
+
if (glob.includes("/")) return false;
|
|
255
|
+
const expression = new RegExp(`^${glob.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, "[^/]*").replace(/\?/g, "[^/]")}$`);
|
|
256
|
+
return expression.test(name);
|
|
257
|
+
}
|
|
258
|
+
function uncoveredPaths(path) {
|
|
259
|
+
const patterns = fs.existsSync(path) ? fs.readFileSync(path, "utf8").split(/\r?\n/) : [];
|
|
260
|
+
return IGNORED_PATHS.filter((ignored) => !patterns.some((pattern) => patternCovers(pattern, ignored)));
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
// src/api.ts
|
|
264
|
+
async function post(fetcher, api, path, body, invalidCode) {
|
|
265
|
+
const url = `${api}${path}`;
|
|
266
|
+
let response;
|
|
267
|
+
try {
|
|
268
|
+
response = await fetcher(url, {
|
|
269
|
+
method: "POST",
|
|
270
|
+
headers: { "content-type": "application/json", accept: "application/json" },
|
|
271
|
+
body: JSON.stringify(body)
|
|
272
|
+
});
|
|
273
|
+
} catch (cause) {
|
|
274
|
+
throw new AuthioCliError("api_unreachable", `Could not reach Authio at ${api}. Check the address (--api) and your connection.`, { cause });
|
|
275
|
+
}
|
|
276
|
+
let envelope = {};
|
|
277
|
+
try {
|
|
278
|
+
envelope = await response.json();
|
|
279
|
+
} catch {
|
|
280
|
+
envelope = {};
|
|
281
|
+
}
|
|
282
|
+
if (response.status === 401)
|
|
283
|
+
throw new AuthioCliError(
|
|
284
|
+
invalidCode,
|
|
285
|
+
invalidCode === "ticket_invalid" ? "This code was already used or has expired. Generate another command in the client's guide in the Authio console." : "This verification has expired. The last step of the client's guide in the console still shows the first sign-in."
|
|
286
|
+
);
|
|
287
|
+
if (response.status === 429) throw new AuthioCliError("rate_limited", "Authio is refusing more attempts for now. Wait a minute and try again.");
|
|
288
|
+
if (!response.ok || envelope.data === void 0) {
|
|
289
|
+
const detail = envelope.notifications?.map((notification) => notification.message).filter(Boolean).join(" ");
|
|
290
|
+
throw new AuthioCliError("api_error", `Authio answered ${response.status}${envelope.errorCode ? ` (${envelope.errorCode})` : ""}${detail ? `: ${detail}` : "."}`);
|
|
291
|
+
}
|
|
292
|
+
return envelope.data;
|
|
293
|
+
}
|
|
294
|
+
async function redeemTicket(fetcher, api, ticket, nextMajor) {
|
|
295
|
+
const redemption = await post(fetcher, api, "/api/v1/integration-plans/redeem", { ticket, nextMajor }, "ticket_invalid");
|
|
296
|
+
return { ...redemption, plan: normalizePlan(redemption.plan) };
|
|
297
|
+
}
|
|
298
|
+
function checkVerification(fetcher, api, verification) {
|
|
299
|
+
return post(fetcher, api, "/api/v1/integration-plans/verify", { verification }, "verification_invalid");
|
|
300
|
+
}
|
|
301
|
+
var VERIFICATION_FILE = path.join(".authio", "verification");
|
|
302
|
+
function installCommand(manager, specs) {
|
|
303
|
+
return manager === "npm" ? ["npm", ["install", ...specs]] : [manager, ["add", ...specs]];
|
|
304
|
+
}
|
|
305
|
+
function envLine(name, value) {
|
|
306
|
+
return value.includes('"') ? `${name}='${value}'` : `${name}="${value}"`;
|
|
307
|
+
}
|
|
308
|
+
function describe(io, project, plan, changes) {
|
|
309
|
+
io.print("");
|
|
310
|
+
io.print(`Authio plan for client "${plan.client}" in realm "${plan.realm}" (${plan.stack})`);
|
|
311
|
+
io.print(`Project: Next.js ${project.nextMajor}${project.srcDir ? ", src/ folder" : ""}, ${project.packageManager}`);
|
|
312
|
+
io.print("");
|
|
313
|
+
for (const file of changes.files) {
|
|
314
|
+
io.print(file.action === "create" ? ` + ${file.path}` : ` ! ${file.path} already exists: left as it is, merge it by hand`);
|
|
315
|
+
for (const line of file.content.split(/\r?\n/)) io.print(` ${line}`);
|
|
316
|
+
}
|
|
317
|
+
io.print("");
|
|
318
|
+
io.print(" .env.local");
|
|
319
|
+
for (const variable of changes.environment) {
|
|
320
|
+
const how = variable.source === "secret" ? "asked for, hidden" : variable.source === "generate" ? "generated on this machine" : variable.value ?? "";
|
|
321
|
+
io.print(` + ${variable.name} ${variable.source === "plan" ? `= ${how}` : `(${how})`}`);
|
|
322
|
+
}
|
|
323
|
+
for (const name of changes.keptEnvironment) io.print(` = ${name} (already set, kept)`);
|
|
324
|
+
if (plan.packages.length > 0) io.print(`
|
|
325
|
+
Packages: ${plan.packages.map((item) => `${item.name}@${item.version}`).join(" ")}`);
|
|
326
|
+
if (!changes.redirectRegistered)
|
|
327
|
+
io.print(`
|
|
328
|
+
Warning: the client does not register ${changes.localRedirect}. Sign-in will fail until you add it as a redirect URI on the client in the Authio console.`);
|
|
329
|
+
if (!plan.clientActive) io.print("\n Warning: this client is inactive or in quarantine in Authio, so sign-in is refused until it is active again.");
|
|
330
|
+
io.print("");
|
|
331
|
+
}
|
|
332
|
+
async function valueOf(io, variable, emptySecrets) {
|
|
333
|
+
if (variable.source === "plan") return variable.value ?? "";
|
|
334
|
+
if (variable.source === "generate") return crypto.randomBytes(32).toString("base64");
|
|
335
|
+
if (!io.isInteractive) {
|
|
336
|
+
emptySecrets.push(variable.name);
|
|
337
|
+
return "";
|
|
338
|
+
}
|
|
339
|
+
const typed = (await io.promptSecret(`${variable.name}${variable.comment ? ` (${variable.comment})` : ""}, hidden, Enter to leave empty: `)).trim();
|
|
340
|
+
if (!typed) emptySecrets.push(variable.name);
|
|
341
|
+
return typed;
|
|
342
|
+
}
|
|
343
|
+
async function runInit(options, io) {
|
|
344
|
+
const result = { exitCode: 0, created: [], toMerge: [], addedEnvironment: [], emptySecrets: [], plan: void 0 };
|
|
345
|
+
const ticket = options.ticket?.trim();
|
|
346
|
+
if (!ticket)
|
|
347
|
+
throw new AuthioCliError("ticket_missing", "No code given. Open the client's guide in the Authio console, choose Next.js \xB7 Auth.js v5 and press Generate command.");
|
|
348
|
+
const project = detectProject(options.cwd);
|
|
349
|
+
if (!options.dryRun && !options.yes && !io.isInteractive)
|
|
350
|
+
throw new AuthioCliError("confirmation_required", "There is no terminal to confirm in. Run with --yes to apply the plan, or --dry-run to see it first.");
|
|
351
|
+
const redemption = await redeemTicket(io.fetch, options.api, ticket, project.nextMajor);
|
|
352
|
+
const plan = redemption.plan;
|
|
353
|
+
result.plan = plan;
|
|
354
|
+
assertPlanVersion(plan);
|
|
355
|
+
const changes = planChanges(project, plan);
|
|
356
|
+
result.toMerge = changes.files.filter((file) => file.action === "merge").map((file) => file.path);
|
|
357
|
+
describe(io, project, plan, changes);
|
|
358
|
+
if (options.dryRun) {
|
|
359
|
+
io.print("Dry run: nothing was written. The code is used up; generate another command to apply the plan.");
|
|
360
|
+
return result;
|
|
361
|
+
}
|
|
362
|
+
const creating = changes.files.filter((file) => file.action === "create");
|
|
363
|
+
const agreed = options.yes || await io.confirm(`Create ${creating.length} file(s) and add ${changes.environment.length} variable(s) to .env.local?`, true);
|
|
364
|
+
if (!agreed) {
|
|
365
|
+
io.print("Nothing was written.");
|
|
366
|
+
result.exitCode = 2;
|
|
367
|
+
return result;
|
|
368
|
+
}
|
|
369
|
+
const lines = [];
|
|
370
|
+
for (const variable of changes.environment) {
|
|
371
|
+
const value = await valueOf(io, variable, result.emptySecrets);
|
|
372
|
+
if (variable.comment) lines.push(`# ${variable.comment}`);
|
|
373
|
+
lines.push(envLine(variable.name, value));
|
|
374
|
+
result.addedEnvironment.push(variable.name);
|
|
375
|
+
}
|
|
376
|
+
for (const file of creating) {
|
|
377
|
+
fs.mkdirSync(path.dirname(file.target), { recursive: true });
|
|
378
|
+
fs.writeFileSync(file.target, file.content.endsWith("\n") ? file.content : `${file.content}
|
|
379
|
+
`, { flag: "wx" });
|
|
380
|
+
result.created.push(file.path);
|
|
381
|
+
}
|
|
382
|
+
if (lines.length > 0) {
|
|
383
|
+
const envPath = path.join(project.root, ".env.local");
|
|
384
|
+
const current = fs.existsSync(envPath) ? fs.readFileSync(envPath, "utf8") : "";
|
|
385
|
+
const separator = current && !current.endsWith("\n") ? "\n" : "";
|
|
386
|
+
fs.appendFileSync(envPath, `${separator}${current ? "\n" : ""}# Added by @hyper.authio/cli for the Authio client "${plan.client}"
|
|
387
|
+
${lines.join("\n")}
|
|
388
|
+
`);
|
|
389
|
+
}
|
|
390
|
+
const gitignorePath = path.join(project.root, ".gitignore");
|
|
391
|
+
const missing = uncoveredPaths(gitignorePath);
|
|
392
|
+
if (missing.length > 0 && (options.yes || await io.confirm(`Add ${missing.join(" and ")} to .gitignore so they are never committed?`, true))) {
|
|
393
|
+
const current = fs.existsSync(gitignorePath) ? fs.readFileSync(gitignorePath, "utf8") : "";
|
|
394
|
+
fs.appendFileSync(gitignorePath, `${current && !current.endsWith("\n") ? "\n" : ""}${missing.join("\n")}
|
|
395
|
+
`);
|
|
396
|
+
} else if (missing.length > 0) {
|
|
397
|
+
io.print(`Warning: ${missing.join(" and ")} ${missing.length > 1 ? "are" : "is"} not in .gitignore. Never commit ${IGNORED_PATHS.join(" or ")}.`);
|
|
398
|
+
}
|
|
399
|
+
const verificationPath = path.join(project.root, VERIFICATION_FILE);
|
|
400
|
+
fs.mkdirSync(path.dirname(verificationPath), { recursive: true });
|
|
401
|
+
fs.writeFileSync(verificationPath, JSON.stringify({ api: options.api, verification: redemption.verification, expiresAt: redemption.verificationExpiresAt }), {
|
|
402
|
+
mode: 384
|
|
403
|
+
});
|
|
404
|
+
try {
|
|
405
|
+
fs.chmodSync(verificationPath, 384);
|
|
406
|
+
} catch {
|
|
407
|
+
}
|
|
408
|
+
if (plan.packages.length > 0) {
|
|
409
|
+
const [command, args] = installCommand(project.packageManager, plan.packages.map((item) => `${item.name}@${item.version}`));
|
|
410
|
+
const install = options.yes || await io.confirm(`Install ${plan.packages.map((item) => item.name).join(", ")} with ${command}?`, true);
|
|
411
|
+
if (install) {
|
|
412
|
+
const code = await io.run(command, args, project.root);
|
|
413
|
+
if (code !== 0) io.print(`The install failed (exit ${code}). Run it yourself: ${command} ${args.join(" ")}`);
|
|
414
|
+
} else {
|
|
415
|
+
io.print(`Install them when you are ready: ${command} ${args.join(" ")}`);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
io.print("");
|
|
419
|
+
io.print(`Created: ${result.created.length > 0 ? result.created.join(", ") : "nothing"}`);
|
|
420
|
+
if (result.toMerge.length > 0) io.print(`Merge by hand: ${result.toMerge.join(", ")} (the plan's version is printed above)`);
|
|
421
|
+
if (result.emptySecrets.length > 0) io.print(`Fill in .env.local: ${result.emptySecrets.join(", ")}`);
|
|
422
|
+
io.print(`Next: start the app, sign in, then run: npx "@hyper.authio/cli" verify`);
|
|
423
|
+
return result;
|
|
424
|
+
}
|
|
425
|
+
var VERIFY_INTERVAL_MS = 3e3;
|
|
426
|
+
function readSaved(cwd) {
|
|
427
|
+
const path$1 = path.join(cwd, VERIFICATION_FILE);
|
|
428
|
+
if (!fs.existsSync(path$1)) return {};
|
|
429
|
+
try {
|
|
430
|
+
return JSON.parse(fs.readFileSync(path$1, "utf8"));
|
|
431
|
+
} catch {
|
|
432
|
+
return {};
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function runVerify(options, io) {
|
|
436
|
+
const saved = readSaved(options.cwd);
|
|
437
|
+
const verification = options.verification ?? saved.verification;
|
|
438
|
+
const api = options.api ?? saved.api ?? options.defaultApi;
|
|
439
|
+
if (!verification)
|
|
440
|
+
throw new AuthioCliError("verification_missing", `No verification found. Run "authio init" first, or pass --verification.`);
|
|
441
|
+
const expiresAt = saved.expiresAt && !options.verification ? Date.parse(saved.expiresAt) : Number.NaN;
|
|
442
|
+
if (options.wait) io.print("Waiting for the first sign-in through the app (Ctrl+C to stop)\u2026");
|
|
443
|
+
for (; ; ) {
|
|
444
|
+
const answer = await checkVerification(io.fetch, api, verification);
|
|
445
|
+
if (answer.signedIn) {
|
|
446
|
+
io.print(`Signed in: the first sign-in arrived${answer.firstSignInAt ? ` at ${answer.firstSignInAt}` : ""}. The integration works.`);
|
|
447
|
+
return { exitCode: 0, firstSignInAt: answer.firstSignInAt };
|
|
448
|
+
}
|
|
449
|
+
if (!options.wait || !Number.isNaN(expiresAt) && io.now() + VERIFY_INTERVAL_MS >= expiresAt) {
|
|
450
|
+
io.print("No sign-in has arrived yet. Start the app, sign in, and run this again (or add --wait).");
|
|
451
|
+
return { exitCode: 2, firstSignInAt: null };
|
|
452
|
+
}
|
|
453
|
+
await io.sleep(VERIFY_INTERVAL_MS);
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
async function main(argv, io, environment, version) {
|
|
457
|
+
try {
|
|
458
|
+
const args = parseArguments(argv);
|
|
459
|
+
const cwd = path.resolve(args.cwd ?? ".");
|
|
460
|
+
if (args.command === "help") {
|
|
461
|
+
io.print(HELP);
|
|
462
|
+
return 0;
|
|
463
|
+
}
|
|
464
|
+
if (args.command === "version") {
|
|
465
|
+
io.print(version);
|
|
466
|
+
return 0;
|
|
467
|
+
}
|
|
468
|
+
if (args.command === "init") {
|
|
469
|
+
const result2 = await runInit(
|
|
470
|
+
{ ticket: args.ticket ?? environment.AUTHIO_TICKET, api: apiAddress(args.api), cwd, dryRun: args.dryRun, yes: args.yes },
|
|
471
|
+
io
|
|
472
|
+
);
|
|
473
|
+
return result2.exitCode;
|
|
474
|
+
}
|
|
475
|
+
const result = await runVerify(
|
|
476
|
+
{ verification: args.verification, api: args.api ? apiAddress(args.api) : void 0, defaultApi: DEFAULT_API, cwd, wait: args.wait },
|
|
477
|
+
io
|
|
478
|
+
);
|
|
479
|
+
return result.exitCode;
|
|
480
|
+
} catch (error) {
|
|
481
|
+
if (error instanceof AuthioCliError) {
|
|
482
|
+
io.print(`Error (${error.code}): ${error.message}`);
|
|
483
|
+
return error.exitCode;
|
|
484
|
+
}
|
|
485
|
+
io.print(error instanceof Error ? error.stack ?? error.message : String(error));
|
|
486
|
+
return 1;
|
|
487
|
+
}
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
exports.AuthioCliError = AuthioCliError;
|
|
491
|
+
exports.DEFAULT_API = DEFAULT_API;
|
|
492
|
+
exports.IGNORED_PATHS = IGNORED_PATHS;
|
|
493
|
+
exports.LOCAL_ORIGIN_VARIABLE = LOCAL_ORIGIN_VARIABLE;
|
|
494
|
+
exports.SUPPORTED_PLAN_VERSIONS = SUPPORTED_PLAN_VERSIONS;
|
|
495
|
+
exports.VERIFICATION_FILE = VERIFICATION_FILE;
|
|
496
|
+
exports.VERIFY_INTERVAL_MS = VERIFY_INTERVAL_MS;
|
|
497
|
+
exports.apiAddress = apiAddress;
|
|
498
|
+
exports.assertPlanVersion = assertPlanVersion;
|
|
499
|
+
exports.checkVerification = checkVerification;
|
|
500
|
+
exports.createTerminalIo = createTerminalIo;
|
|
501
|
+
exports.detectProject = detectProject;
|
|
502
|
+
exports.installCommand = installCommand;
|
|
503
|
+
exports.localOrigin = localOrigin;
|
|
504
|
+
exports.main = main;
|
|
505
|
+
exports.normalizePlan = normalizePlan;
|
|
506
|
+
exports.parseArguments = parseArguments;
|
|
507
|
+
exports.planChanges = planChanges;
|
|
508
|
+
exports.readEnvKeys = readEnvKeys;
|
|
509
|
+
exports.redeemTicket = redeemTicket;
|
|
510
|
+
exports.runInit = runInit;
|
|
511
|
+
exports.runVerify = runVerify;
|
|
512
|
+
exports.uncoveredPaths = uncoveredPaths;
|
|
513
|
+
exports.windowsCommandLine = windowsCommandLine;
|
|
514
|
+
//# sourceMappingURL=index.cjs.map
|
|
515
|
+
//# sourceMappingURL=index.cjs.map
|