@ratio-app/cli 0.1.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 +504 -0
- package/bin/run.mjs +5 -0
- package/dist/commands/__test__/throw.d.ts +25 -0
- package/dist/commands/__test__/throw.js +238 -0
- package/dist/commands/__test__/throw.js.map +1 -0
- package/dist/commands/app/create.d.ts +55 -0
- package/dist/commands/app/create.js +609 -0
- package/dist/commands/app/create.js.map +1 -0
- package/dist/commands/app/deploy.d.ts +14 -0
- package/dist/commands/app/deploy.js +179 -0
- package/dist/commands/app/deploy.js.map +1 -0
- package/dist/commands/app/dev.d.ts +28 -0
- package/dist/commands/app/dev.js +701 -0
- package/dist/commands/app/dev.js.map +1 -0
- package/dist/commands/app/generate.d.ts +14 -0
- package/dist/commands/app/generate.js +179 -0
- package/dist/commands/app/generate.js.map +1 -0
- package/dist/commands/app/info.d.ts +14 -0
- package/dist/commands/app/info.js +179 -0
- package/dist/commands/app/info.js.map +1 -0
- package/dist/commands/app/link.d.ts +118 -0
- package/dist/commands/app/link.js +1120 -0
- package/dist/commands/app/link.js.map +1 -0
- package/dist/commands/auth/login.d.ts +43 -0
- package/dist/commands/auth/login.js +987 -0
- package/dist/commands/auth/login.js.map +1 -0
- package/dist/commands/auth/logout.d.ts +15 -0
- package/dist/commands/auth/logout.js +486 -0
- package/dist/commands/auth/logout.js.map +1 -0
- package/dist/commands/auth/whoami.d.ts +16 -0
- package/dist/commands/auth/whoami.js +531 -0
- package/dist/commands/auth/whoami.js.map +1 -0
- package/dist/commands/dev/listen.d.ts +84 -0
- package/dist/commands/dev/listen.js +1187 -0
- package/dist/commands/dev/listen.js.map +1 -0
- package/dist/commands/dev/trigger.d.ts +88 -0
- package/dist/commands/dev/trigger.js +729 -0
- package/dist/commands/dev/trigger.js.map +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +617 -0
- package/dist/index.js.map +1 -0
- package/dist/lib/auth/authorize-code-exchange.d.ts +50 -0
- package/dist/lib/auth/authorize-code-exchange.js +114 -0
- package/dist/lib/auth/authorize-code-exchange.js.map +1 -0
- package/dist/lib/auth/authorize-url.d.ts +42 -0
- package/dist/lib/auth/authorize-url.js +35 -0
- package/dist/lib/auth/authorize-url.js.map +1 -0
- package/dist/lib/auth/browser.d.ts +16 -0
- package/dist/lib/auth/browser.js +48 -0
- package/dist/lib/auth/browser.js.map +1 -0
- package/dist/lib/auth/config.d.ts +41 -0
- package/dist/lib/auth/config.js +21 -0
- package/dist/lib/auth/config.js.map +1 -0
- package/dist/lib/auth/http-refresher.d.ts +30 -0
- package/dist/lib/auth/http-refresher.js +103 -0
- package/dist/lib/auth/http-refresher.js.map +1 -0
- package/dist/lib/auth/identity.d.ts +26 -0
- package/dist/lib/auth/identity.js +10 -0
- package/dist/lib/auth/identity.js.map +1 -0
- package/dist/lib/auth/index.d.ts +13 -0
- package/dist/lib/auth/index.js +527 -0
- package/dist/lib/auth/index.js.map +1 -0
- package/dist/lib/auth/loopback-server.d.ts +65 -0
- package/dist/lib/auth/loopback-server.js +245 -0
- package/dist/lib/auth/loopback-server.js.map +1 -0
- package/dist/lib/auth/manual-prompt.d.ts +17 -0
- package/dist/lib/auth/manual-prompt.js +20 -0
- package/dist/lib/auth/manual-prompt.js.map +1 -0
- package/dist/lib/auth/pkce.d.ts +24 -0
- package/dist/lib/auth/pkce.js +15 -0
- package/dist/lib/auth/pkce.js.map +1 -0
- package/dist/lib/auth/state.d.ts +20 -0
- package/dist/lib/auth/state.js +19 -0
- package/dist/lib/auth/state.js.map +1 -0
- package/dist/lib/auth/verbose-trace.d.ts +25 -0
- package/dist/lib/auth/verbose-trace.js +17 -0
- package/dist/lib/auth/verbose-trace.js.map +1 -0
- package/dist/lib/credentials/clock.d.ts +18 -0
- package/dist/lib/credentials/clock.js +10 -0
- package/dist/lib/credentials/clock.js.map +1 -0
- package/dist/lib/credentials/index.d.ts +5 -0
- package/dist/lib/credentials/index.js +340 -0
- package/dist/lib/credentials/index.js.map +1 -0
- package/dist/lib/credentials/path.d.ts +26 -0
- package/dist/lib/credentials/path.js +32 -0
- package/dist/lib/credentials/path.js.map +1 -0
- package/dist/lib/credentials/refresher.d.ts +26 -0
- package/dist/lib/credentials/refresher.js +34 -0
- package/dist/lib/credentials/refresher.js.map +1 -0
- package/dist/lib/credentials/schema.d.ts +138 -0
- package/dist/lib/credentials/schema.js +85 -0
- package/dist/lib/credentials/schema.js.map +1 -0
- package/dist/lib/credentials/store.d.ts +102 -0
- package/dist/lib/credentials/store.js +337 -0
- package/dist/lib/credentials/store.js.map +1 -0
- package/dist/lib/credentials/types.d.ts +65 -0
- package/dist/lib/credentials/types.js +1 -0
- package/dist/lib/credentials/types.js.map +1 -0
- package/dist/lib/dev-store-gate.d.ts +66 -0
- package/dist/lib/dev-store-gate.js +15 -0
- package/dist/lib/dev-store-gate.js.map +1 -0
- package/dist/lib/errors.d.ts +60 -0
- package/dist/lib/errors.js +101 -0
- package/dist/lib/errors.js.map +1 -0
- package/dist/lib/manifest/index.d.ts +2 -0
- package/dist/lib/manifest/index.js +144 -0
- package/dist/lib/manifest/index.js.map +1 -0
- package/dist/lib/manifest/read-manifest.d.ts +32 -0
- package/dist/lib/manifest/read-manifest.js +87 -0
- package/dist/lib/manifest/read-manifest.js.map +1 -0
- package/dist/lib/manifest/write-client-id.d.ts +33 -0
- package/dist/lib/manifest/write-client-id.js +94 -0
- package/dist/lib/manifest/write-client-id.js.map +1 -0
- package/dist/lib/messages.d.ts +15 -0
- package/dist/lib/messages.js +16 -0
- package/dist/lib/messages.js.map +1 -0
- package/dist/lib/orchestrator/child-runner.d.ts +140 -0
- package/dist/lib/orchestrator/child-runner.js +471 -0
- package/dist/lib/orchestrator/child-runner.js.map +1 -0
- package/dist/lib/preview/index.d.ts +11 -0
- package/dist/lib/preview/index.js +17 -0
- package/dist/lib/preview/index.js.map +1 -0
- package/dist/lib/preview/types.d.ts +16 -0
- package/dist/lib/preview/types.js +11 -0
- package/dist/lib/preview/types.js.map +1 -0
- package/dist/lib/ratio-command.d.ts +52 -0
- package/dist/lib/ratio-command.js +141 -0
- package/dist/lib/ratio-command.js.map +1 -0
- package/dist/lib/relay/index.d.ts +7 -0
- package/dist/lib/relay/index.js +362 -0
- package/dist/lib/relay/index.js.map +1 -0
- package/dist/lib/relay/relay-client.d.ts +91 -0
- package/dist/lib/relay/relay-client.js +362 -0
- package/dist/lib/relay/relay-client.js.map +1 -0
- package/dist/lib/relay/types.d.ts +71 -0
- package/dist/lib/relay/types.js +1 -0
- package/dist/lib/relay/types.js.map +1 -0
- package/dist/lib/render-error.d.ts +35 -0
- package/dist/lib/render-error.js +115 -0
- package/dist/lib/render-error.js.map +1 -0
- package/dist/lib/verbose-flag.d.ts +12 -0
- package/dist/lib/verbose-flag.js +17 -0
- package/dist/lib/verbose-flag.js.map +1 -0
- package/dist/render-CrHGqTPH.d.ts +115 -0
- package/dist/templates/.gitkeep +0 -0
- package/dist/templates/minimal/.env.example.tmpl +4 -0
- package/dist/templates/minimal/README.md.tmpl +26 -0
- package/dist/templates/minimal/gitignore +5 -0
- package/dist/templates/minimal/manifest.json +5 -0
- package/dist/templates/minimal/package.json.tmpl +25 -0
- package/dist/templates/minimal/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/minimal/src/index.ts.tmpl +26 -0
- package/dist/templates/minimal/src/install.ts.tmpl +37 -0
- package/dist/templates/minimal/src/webhooks.ts.tmpl +57 -0
- package/dist/templates/minimal/tsconfig.json +17 -0
- package/dist/templates/serverless/README.md.tmpl +28 -0
- package/dist/templates/serverless/gitignore +6 -0
- package/dist/templates/serverless/manifest.json +5 -0
- package/dist/templates/serverless/package.json.tmpl +22 -0
- package/dist/templates/serverless/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/serverless/src/index.ts.tmpl +20 -0
- package/dist/templates/serverless/src/install.ts.tmpl +20 -0
- package/dist/templates/serverless/src/webhooks.ts.tmpl +36 -0
- package/dist/templates/serverless/tsconfig.json +18 -0
- package/dist/templates/serverless/wrangler.toml.tmpl +9 -0
- package/dist/templates/with-admin-ui/.env.example.tmpl +4 -0
- package/dist/templates/with-admin-ui/README.md.tmpl +28 -0
- package/dist/templates/with-admin-ui/gitignore +6 -0
- package/dist/templates/with-admin-ui/manifest.json +5 -0
- package/dist/templates/with-admin-ui/package.json.tmpl +31 -0
- package/dist/templates/with-admin-ui/ratio.config.jsonc.tmpl +17 -0
- package/dist/templates/with-admin-ui/src/admin/app/globals.css +19 -0
- package/dist/templates/with-admin-ui/src/admin/app/layout.tsx.tmpl +13 -0
- package/dist/templates/with-admin-ui/src/admin/app/page.tsx.tmpl +8 -0
- package/dist/templates/with-admin-ui/src/admin/next-env.d.ts.tmpl +5 -0
- package/dist/templates/with-admin-ui/src/admin/next.config.mjs +3 -0
- package/dist/templates/with-admin-ui/src/admin/tsconfig.json +20 -0
- package/dist/templates/with-admin-ui/src/index.ts.tmpl +26 -0
- package/dist/templates/with-admin-ui/src/install.ts.tmpl +37 -0
- package/dist/templates/with-admin-ui/src/webhooks.ts.tmpl +57 -0
- package/dist/templates/with-admin-ui/tsconfig.json +17 -0
- package/package.json +78 -0
|
@@ -0,0 +1,987 @@
|
|
|
1
|
+
// src/commands/auth/login.ts
|
|
2
|
+
import { Flags as Flags2, ux } from "@oclif/core";
|
|
3
|
+
|
|
4
|
+
// src/lib/ratio-command.ts
|
|
5
|
+
import { Command, Errors } from "@oclif/core";
|
|
6
|
+
|
|
7
|
+
// src/lib/errors.ts
|
|
8
|
+
var RatioCLIError = class extends Error {
|
|
9
|
+
code;
|
|
10
|
+
exitCode;
|
|
11
|
+
hint;
|
|
12
|
+
docsUrl;
|
|
13
|
+
constructor(opts) {
|
|
14
|
+
super(opts.message);
|
|
15
|
+
this.name = new.target.name;
|
|
16
|
+
this.code = opts.code;
|
|
17
|
+
this.exitCode = opts.exitCode;
|
|
18
|
+
if (opts.hint !== void 0) this.hint = opts.hint;
|
|
19
|
+
if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var NotLoggedInError = class extends RatioCLIError {
|
|
23
|
+
constructor(message, hint = "Run 'ratio auth login' to sign in.") {
|
|
24
|
+
super({ code: "NOT_LOGGED_IN", exitCode: 3, message, hint });
|
|
25
|
+
}
|
|
26
|
+
};
|
|
27
|
+
var TokenExpiredError = class extends RatioCLIError {
|
|
28
|
+
constructor(message, hint = "Run 'ratio auth login' to refresh your session.") {
|
|
29
|
+
super({ code: "TOKEN_EXPIRED", exitCode: 3, message, hint });
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var NetworkError = class extends RatioCLIError {
|
|
33
|
+
constructor(message, hint = "Check your network and retry.") {
|
|
34
|
+
super({ code: "NETWORK_ERROR", exitCode: 5, message, hint });
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
var ValidationError = class extends RatioCLIError {
|
|
38
|
+
constructor(message, hint) {
|
|
39
|
+
const opts = {
|
|
40
|
+
code: "VALIDATION_ERROR",
|
|
41
|
+
exitCode: 8,
|
|
42
|
+
message
|
|
43
|
+
};
|
|
44
|
+
if (hint !== void 0) opts.hint = hint;
|
|
45
|
+
super(opts);
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
function isRatioCLIError(err) {
|
|
49
|
+
return err instanceof RatioCLIError;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// src/lib/render-error.ts
|
|
53
|
+
function isOclifParserError(err) {
|
|
54
|
+
return err instanceof Error && "parse" in err && err.parse !== null && err.parse !== void 0;
|
|
55
|
+
}
|
|
56
|
+
function buildErrorEnvelope(code, message, hint, docsUrl) {
|
|
57
|
+
const obj = { ok: false, code, message };
|
|
58
|
+
if (hint !== void 0) obj["hint"] = hint;
|
|
59
|
+
if (docsUrl !== void 0) obj["docsUrl"] = docsUrl;
|
|
60
|
+
return JSON.stringify(obj) + "\n";
|
|
61
|
+
}
|
|
62
|
+
function renderError(input2) {
|
|
63
|
+
const { err, verbose, json } = input2;
|
|
64
|
+
if (isRatioCLIError(err)) {
|
|
65
|
+
const ratioErr = err;
|
|
66
|
+
if (ratioErr.exitCode === 0) {
|
|
67
|
+
if (json) {
|
|
68
|
+
return {
|
|
69
|
+
stdout: JSON.stringify({ ok: true, data: null }) + "\n",
|
|
70
|
+
stderr: "",
|
|
71
|
+
exitCode: 0
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
return {
|
|
75
|
+
stdout: ratioErr.message + "\n",
|
|
76
|
+
stderr: "",
|
|
77
|
+
exitCode: 0
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
let stderrLines = `${ratioErr.code}: ${ratioErr.message}
|
|
81
|
+
`;
|
|
82
|
+
if (ratioErr.hint !== void 0) stderrLines += `${ratioErr.hint}
|
|
83
|
+
`;
|
|
84
|
+
if (ratioErr.docsUrl !== void 0) stderrLines += `${ratioErr.docsUrl}
|
|
85
|
+
`;
|
|
86
|
+
if (json) {
|
|
87
|
+
return {
|
|
88
|
+
stdout: buildErrorEnvelope(ratioErr.code, ratioErr.message, ratioErr.hint, ratioErr.docsUrl),
|
|
89
|
+
stderr: verbose && ratioErr.stack ? ratioErr.stack + "\n" : "",
|
|
90
|
+
exitCode: ratioErr.exitCode
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
if (verbose && ratioErr.stack) {
|
|
94
|
+
stderrLines += ratioErr.stack + "\n";
|
|
95
|
+
}
|
|
96
|
+
return { stdout: "", stderr: stderrLines, exitCode: ratioErr.exitCode };
|
|
97
|
+
}
|
|
98
|
+
if (isOclifParserError(err)) {
|
|
99
|
+
const parserErr = err;
|
|
100
|
+
const msg = parserErr.message || "Unknown parse error";
|
|
101
|
+
const coercedErr = new ValidationError(msg);
|
|
102
|
+
let stderrText2 = `${coercedErr.code}: ${msg}
|
|
103
|
+
`;
|
|
104
|
+
if (json) {
|
|
105
|
+
return {
|
|
106
|
+
stdout: buildErrorEnvelope(coercedErr.code, msg),
|
|
107
|
+
stderr: verbose && parserErr.stack ? parserErr.stack + "\n" : "",
|
|
108
|
+
exitCode: 8
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
if (verbose && parserErr.stack) {
|
|
112
|
+
stderrText2 += parserErr.stack + "\n";
|
|
113
|
+
}
|
|
114
|
+
return { stdout: "", stderr: stderrText2, exitCode: 8 };
|
|
115
|
+
}
|
|
116
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
117
|
+
const stack = err instanceof Error ? err.stack : void 0;
|
|
118
|
+
const stderrPrefix = `INTERNAL: ${message}
|
|
119
|
+
`;
|
|
120
|
+
if (json) {
|
|
121
|
+
return {
|
|
122
|
+
stdout: buildErrorEnvelope("INTERNAL", message),
|
|
123
|
+
stderr: verbose && stack ? stack + "\n" : "",
|
|
124
|
+
exitCode: 1
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
let stderrText = stderrPrefix;
|
|
128
|
+
if (verbose && stack) {
|
|
129
|
+
stderrText += stack + "\n";
|
|
130
|
+
}
|
|
131
|
+
return { stdout: "", stderr: stderrText, exitCode: 1 };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// src/lib/ratio-command.ts
|
|
135
|
+
var RatioCommand = class extends Command {
|
|
136
|
+
_getRenderFlags() {
|
|
137
|
+
const argv = this.argv;
|
|
138
|
+
return {
|
|
139
|
+
verbose: argv.includes("--verbose"),
|
|
140
|
+
json: argv.includes("--json")
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
async catch(err) {
|
|
144
|
+
const { verbose, json } = this._getRenderFlags();
|
|
145
|
+
const output2 = renderError({ err, verbose, json });
|
|
146
|
+
if (output2.stdout) process.stdout.write(output2.stdout);
|
|
147
|
+
if (output2.stderr) process.stderr.write(output2.stderr);
|
|
148
|
+
if (output2.exitCode === 0) {
|
|
149
|
+
throw new Errors.ExitError(0);
|
|
150
|
+
}
|
|
151
|
+
err.skipOclifErrorHandling = true;
|
|
152
|
+
err.oclif = { exit: output2.exitCode };
|
|
153
|
+
throw err;
|
|
154
|
+
}
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
// src/lib/verbose-flag.ts
|
|
158
|
+
import { Flags } from "@oclif/core";
|
|
159
|
+
var verboseFlag = {
|
|
160
|
+
verbose: Flags.boolean({
|
|
161
|
+
description: "Show diagnostic detail: HTTP status/timing lines for auth requests (stderr) and stack traces for internal errors."
|
|
162
|
+
})
|
|
163
|
+
};
|
|
164
|
+
var jsonFlag = {
|
|
165
|
+
json: Flags.boolean({
|
|
166
|
+
description: "Emit machine-readable JSON on stdout."
|
|
167
|
+
})
|
|
168
|
+
};
|
|
169
|
+
|
|
170
|
+
// src/lib/credentials/clock.ts
|
|
171
|
+
var SystemClock = class {
|
|
172
|
+
now() {
|
|
173
|
+
return Date.now();
|
|
174
|
+
}
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
// src/lib/credentials/path.ts
|
|
178
|
+
import os from "os";
|
|
179
|
+
import path from "path";
|
|
180
|
+
function snapshotPathEnv() {
|
|
181
|
+
const snapshot = {};
|
|
182
|
+
const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
|
|
183
|
+
if (xdgConfigHome !== void 0) {
|
|
184
|
+
snapshot.XDG_CONFIG_HOME = xdgConfigHome;
|
|
185
|
+
}
|
|
186
|
+
const appData = process.env["APPDATA"];
|
|
187
|
+
if (appData !== void 0) {
|
|
188
|
+
snapshot.APPDATA = appData;
|
|
189
|
+
}
|
|
190
|
+
return snapshot;
|
|
191
|
+
}
|
|
192
|
+
function defaultCredentialsPath(platform, homedir, env) {
|
|
193
|
+
const resolvedPlatform = platform ?? process.platform;
|
|
194
|
+
const resolvedHome = homedir ?? os.homedir();
|
|
195
|
+
const resolvedEnv = env ?? snapshotPathEnv();
|
|
196
|
+
if (resolvedPlatform === "win32") {
|
|
197
|
+
const appData = resolvedEnv.APPDATA;
|
|
198
|
+
const base2 = appData !== void 0 && appData !== "" ? appData : path.win32.join(resolvedHome, "AppData", "Roaming");
|
|
199
|
+
return path.win32.join(base2, "ratio", "credentials");
|
|
200
|
+
}
|
|
201
|
+
const xdgConfigHome = resolvedEnv.XDG_CONFIG_HOME;
|
|
202
|
+
const base = typeof xdgConfigHome === "string" && xdgConfigHome !== "" ? xdgConfigHome : path.posix.join(resolvedHome, ".config");
|
|
203
|
+
return path.posix.join(base, "ratio", "credentials");
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/lib/credentials/refresher.ts
|
|
207
|
+
var NotImplementedRefresher = class {
|
|
208
|
+
async refresh(refreshToken) {
|
|
209
|
+
void refreshToken;
|
|
210
|
+
throw new NotLoggedInError(
|
|
211
|
+
"Token refresh is not available yet in this CLI version."
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
// src/lib/credentials/store.ts
|
|
217
|
+
import crypto from "crypto";
|
|
218
|
+
import fs from "fs";
|
|
219
|
+
import { dirname } from "path";
|
|
220
|
+
|
|
221
|
+
// src/lib/credentials/schema.ts
|
|
222
|
+
import { z } from "zod";
|
|
223
|
+
var identitySchema = z.object({
|
|
224
|
+
developerId: z.string(),
|
|
225
|
+
email: z.string(),
|
|
226
|
+
// Token-derived fields captured at login. Optional (additive) so files
|
|
227
|
+
// written before these fields existed still validate as version 1.
|
|
228
|
+
merchantId: z.string().optional(),
|
|
229
|
+
scopes: z.array(z.string()).optional()
|
|
230
|
+
}).strict();
|
|
231
|
+
var profileSchema = z.object({
|
|
232
|
+
accessToken: z.string(),
|
|
233
|
+
refreshToken: z.string(),
|
|
234
|
+
expiresAt: z.string().datetime({ offset: true }),
|
|
235
|
+
identity: identitySchema,
|
|
236
|
+
obtainedAt: z.string().datetime({ offset: true })
|
|
237
|
+
}).strict();
|
|
238
|
+
var developerSessionSchema = z.object({
|
|
239
|
+
jwt: z.string(),
|
|
240
|
+
developerId: z.string(),
|
|
241
|
+
email: z.string(),
|
|
242
|
+
expiresAt: z.string().datetime({ offset: true }),
|
|
243
|
+
obtainedAt: z.string().datetime({ offset: true })
|
|
244
|
+
}).strict();
|
|
245
|
+
var credentialsSchema = z.object({
|
|
246
|
+
version: z.literal(1),
|
|
247
|
+
server: z.string(),
|
|
248
|
+
clientId: z.string(),
|
|
249
|
+
profiles: z.record(z.string(), profileSchema),
|
|
250
|
+
// Optional cached developer session. Additive so files written before
|
|
251
|
+
// this field existed still validate as version 1.
|
|
252
|
+
developer: developerSessionSchema.optional()
|
|
253
|
+
}).strict();
|
|
254
|
+
function parseCredentials(input2) {
|
|
255
|
+
if (typeof input2 === "object" && input2 !== null && "version" in input2) {
|
|
256
|
+
const version = input2.version;
|
|
257
|
+
if (version !== 1) {
|
|
258
|
+
const label = typeof version === "number" ? String(version) : "unknown";
|
|
259
|
+
throw new ValidationError(
|
|
260
|
+
`credentials file version ${label} is unsupported by this CLI \u2014 upgrade the CLI or sign in again`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
const result = credentialsSchema.safeParse(input2);
|
|
265
|
+
if (!result.success) {
|
|
266
|
+
throw new ValidationError(
|
|
267
|
+
"credentials file schema mismatch \u2014 expected v1 shape"
|
|
268
|
+
);
|
|
269
|
+
}
|
|
270
|
+
return result.data;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// src/lib/credentials/store.ts
|
|
274
|
+
var REFRESH_WINDOW_MS = 3e5;
|
|
275
|
+
var ENV_TOKEN_VAR = "RATIO_API_TOKEN";
|
|
276
|
+
var ENV_DEVELOPER_TOKEN_VAR = "RATIO_DEVELOPER_TOKEN";
|
|
277
|
+
function hasErrnoCode(err, code) {
|
|
278
|
+
return err instanceof Error && err.code === code;
|
|
279
|
+
}
|
|
280
|
+
var FileCredentialStore = class {
|
|
281
|
+
filePath;
|
|
282
|
+
clock;
|
|
283
|
+
refresher;
|
|
284
|
+
envReader;
|
|
285
|
+
platform;
|
|
286
|
+
constructor(opts) {
|
|
287
|
+
this.filePath = opts?.filePath ?? defaultCredentialsPath();
|
|
288
|
+
this.clock = opts?.clock ?? new SystemClock();
|
|
289
|
+
this.refresher = opts?.refresher ?? new NotImplementedRefresher();
|
|
290
|
+
this.envReader = opts?.envReader ?? (() => process.env);
|
|
291
|
+
this.platform = opts?.platform ?? process.platform;
|
|
292
|
+
}
|
|
293
|
+
async load() {
|
|
294
|
+
this.reapplyFileModeIfDrifted();
|
|
295
|
+
let raw;
|
|
296
|
+
try {
|
|
297
|
+
raw = fs.readFileSync(this.filePath, "utf8");
|
|
298
|
+
} catch (err) {
|
|
299
|
+
if (hasErrnoCode(err, "ENOENT")) {
|
|
300
|
+
throw new NotLoggedInError("You are not signed in.");
|
|
301
|
+
}
|
|
302
|
+
throw err;
|
|
303
|
+
}
|
|
304
|
+
let parsed;
|
|
305
|
+
try {
|
|
306
|
+
parsed = JSON.parse(raw);
|
|
307
|
+
} catch {
|
|
308
|
+
throw new ValidationError(
|
|
309
|
+
'credentials file is not valid JSON \u2014 run "ratio auth login" to reset'
|
|
310
|
+
);
|
|
311
|
+
}
|
|
312
|
+
return parseCredentials(parsed);
|
|
313
|
+
}
|
|
314
|
+
async save(creds) {
|
|
315
|
+
const dir = dirname(this.filePath);
|
|
316
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
317
|
+
if (this.platform !== "win32") {
|
|
318
|
+
fs.chmodSync(dir, 448);
|
|
319
|
+
}
|
|
320
|
+
const tmpPath = `${this.filePath}.${process.pid}.${crypto.randomBytes(8).toString("hex")}.tmp`;
|
|
321
|
+
try {
|
|
322
|
+
fs.writeFileSync(tmpPath, JSON.stringify(creds, null, 2), {
|
|
323
|
+
mode: 384
|
|
324
|
+
});
|
|
325
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
326
|
+
} catch (err) {
|
|
327
|
+
try {
|
|
328
|
+
fs.unlinkSync(tmpPath);
|
|
329
|
+
} catch {
|
|
330
|
+
}
|
|
331
|
+
throw err;
|
|
332
|
+
}
|
|
333
|
+
if (this.platform !== "win32") {
|
|
334
|
+
fs.chmodSync(this.filePath, 384);
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
async clear() {
|
|
338
|
+
try {
|
|
339
|
+
fs.unlinkSync(this.filePath);
|
|
340
|
+
} catch (err) {
|
|
341
|
+
if (hasErrnoCode(err, "ENOENT")) {
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
344
|
+
throw err;
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
async getValidAccessToken(profileName) {
|
|
348
|
+
const envToken = this.envReader()[ENV_TOKEN_VAR];
|
|
349
|
+
if (typeof envToken === "string" && envToken.length > 0) {
|
|
350
|
+
return envToken;
|
|
351
|
+
}
|
|
352
|
+
const creds = await this.load();
|
|
353
|
+
const name = profileName ?? "default";
|
|
354
|
+
const profile = creds.profiles[name];
|
|
355
|
+
if (profile === void 0) {
|
|
356
|
+
throw new NotLoggedInError("You are not signed in.");
|
|
357
|
+
}
|
|
358
|
+
const msUntilExpiry = new Date(profile.expiresAt).getTime() - this.clock.now();
|
|
359
|
+
if (msUntilExpiry < REFRESH_WINDOW_MS) {
|
|
360
|
+
return this.refreshAndPersist(creds, name, profile);
|
|
361
|
+
}
|
|
362
|
+
return profile.accessToken;
|
|
363
|
+
}
|
|
364
|
+
async getValidDeveloperToken() {
|
|
365
|
+
const envToken = this.envReader()[ENV_DEVELOPER_TOKEN_VAR];
|
|
366
|
+
if (typeof envToken === "string" && envToken.length > 0) {
|
|
367
|
+
return envToken;
|
|
368
|
+
}
|
|
369
|
+
let creds;
|
|
370
|
+
try {
|
|
371
|
+
creds = await this.load();
|
|
372
|
+
} catch (err) {
|
|
373
|
+
if (err instanceof NotLoggedInError) {
|
|
374
|
+
throw new NotLoggedInError("Developer signin required.");
|
|
375
|
+
}
|
|
376
|
+
throw err;
|
|
377
|
+
}
|
|
378
|
+
const session = creds.developer;
|
|
379
|
+
if (session === void 0) {
|
|
380
|
+
throw new NotLoggedInError("Developer signin required.");
|
|
381
|
+
}
|
|
382
|
+
const msUntilExpiry = new Date(session.expiresAt).getTime() - this.clock.now();
|
|
383
|
+
if (msUntilExpiry < REFRESH_WINDOW_MS) {
|
|
384
|
+
throw new NotLoggedInError("Developer signin required.");
|
|
385
|
+
}
|
|
386
|
+
return session.jwt;
|
|
387
|
+
}
|
|
388
|
+
async saveDeveloperSession(session) {
|
|
389
|
+
let creds;
|
|
390
|
+
try {
|
|
391
|
+
creds = await this.load();
|
|
392
|
+
} catch (err) {
|
|
393
|
+
if (err instanceof NotLoggedInError) {
|
|
394
|
+
creds = { version: 1, server: "", clientId: "", profiles: {} };
|
|
395
|
+
} else {
|
|
396
|
+
throw err;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
const updated = {
|
|
400
|
+
...creds,
|
|
401
|
+
developer: session
|
|
402
|
+
};
|
|
403
|
+
await this.save(updated);
|
|
404
|
+
}
|
|
405
|
+
/**
|
|
406
|
+
* Exchange the profile's refresh token for a fresh token set, merge it
|
|
407
|
+
* into the loaded credentials (preserving every other profile), persist
|
|
408
|
+
* atomically, and return the new access token. A refresher failure is
|
|
409
|
+
* wrapped in TokenExpiredError; the underlying message is deliberately
|
|
410
|
+
* discarded so a secret can never leak through the error surface.
|
|
411
|
+
*/
|
|
412
|
+
async refreshAndPersist(creds, name, profile) {
|
|
413
|
+
let refreshed;
|
|
414
|
+
try {
|
|
415
|
+
refreshed = await this.refresher.refresh(profile.refreshToken);
|
|
416
|
+
} catch {
|
|
417
|
+
throw new TokenExpiredError(
|
|
418
|
+
"Your session has expired and could not be refreshed."
|
|
419
|
+
);
|
|
420
|
+
}
|
|
421
|
+
const updated = {
|
|
422
|
+
...creds,
|
|
423
|
+
profiles: {
|
|
424
|
+
...creds.profiles,
|
|
425
|
+
[name]: {
|
|
426
|
+
...profile,
|
|
427
|
+
accessToken: refreshed.accessToken,
|
|
428
|
+
refreshToken: refreshed.refreshToken,
|
|
429
|
+
expiresAt: refreshed.expiresAt
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
};
|
|
433
|
+
await this.save(updated);
|
|
434
|
+
return refreshed.accessToken;
|
|
435
|
+
}
|
|
436
|
+
/**
|
|
437
|
+
* POSIX-only startup permission check: if the file exists with a mode
|
|
438
|
+
* other than 0600, re-apply 0600 and emit a warning to stderr. Never
|
|
439
|
+
* runs on Windows (stat modes there do not map to POSIX bits and chmod
|
|
440
|
+
* must not be called). Missing file is NOT an error here — load()
|
|
441
|
+
* reports it as NotLoggedInError.
|
|
442
|
+
*/
|
|
443
|
+
reapplyFileModeIfDrifted() {
|
|
444
|
+
if (this.platform === "win32") {
|
|
445
|
+
return;
|
|
446
|
+
}
|
|
447
|
+
let stat;
|
|
448
|
+
try {
|
|
449
|
+
stat = fs.statSync(this.filePath);
|
|
450
|
+
} catch (err) {
|
|
451
|
+
if (hasErrnoCode(err, "ENOENT")) {
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
throw err;
|
|
455
|
+
}
|
|
456
|
+
const mode = stat.mode & 511;
|
|
457
|
+
if (mode !== 384) {
|
|
458
|
+
fs.chmodSync(this.filePath, 384);
|
|
459
|
+
process.stderr.write(
|
|
460
|
+
`[warn] credentials file permissions were 0o${mode.toString(8)}; corrected to 0600.
|
|
461
|
+
`
|
|
462
|
+
);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
};
|
|
466
|
+
|
|
467
|
+
// src/lib/auth/pkce.ts
|
|
468
|
+
import crypto2 from "crypto";
|
|
469
|
+
function computeS256Challenge(codeVerifier) {
|
|
470
|
+
return crypto2.createHash("sha256").update(codeVerifier, "ascii").digest("base64url");
|
|
471
|
+
}
|
|
472
|
+
function generatePkcePair() {
|
|
473
|
+
const codeVerifier = crypto2.randomBytes(96).toString("base64url");
|
|
474
|
+
const codeChallenge = computeS256Challenge(codeVerifier);
|
|
475
|
+
return { codeVerifier, codeChallenge, codeChallengeMethod: "S256" };
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/lib/auth/state.ts
|
|
479
|
+
import crypto3 from "crypto";
|
|
480
|
+
function generateState() {
|
|
481
|
+
return crypto3.randomBytes(32).toString("base64url");
|
|
482
|
+
}
|
|
483
|
+
function timingSafeEqualState(a, b) {
|
|
484
|
+
const aBuf = Buffer.from(a, "utf8");
|
|
485
|
+
const bBuf = Buffer.from(b, "utf8");
|
|
486
|
+
if (aBuf.length !== bBuf.length) {
|
|
487
|
+
crypto3.timingSafeEqual(aBuf, aBuf);
|
|
488
|
+
return false;
|
|
489
|
+
}
|
|
490
|
+
return crypto3.timingSafeEqual(aBuf, bBuf);
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
// src/lib/auth/config.ts
|
|
494
|
+
var DEFAULT_SERVER_URL = "https://gkx.ratio.win";
|
|
495
|
+
var RATIO_CLI_MERCHANT_ID = "ratio-cli-dev";
|
|
496
|
+
var RATIO_CLI_CLIENT_ID = "ratio-cli";
|
|
497
|
+
function resolveServerUrl(opts) {
|
|
498
|
+
const env = opts?.env ?? process.env;
|
|
499
|
+
if (typeof env["RATIO_SERVER_URL"] === "string" && env["RATIO_SERVER_URL"].length > 0) {
|
|
500
|
+
return env["RATIO_SERVER_URL"];
|
|
501
|
+
}
|
|
502
|
+
if (typeof opts?.credentials?.server === "string" && opts.credentials.server.length > 0) {
|
|
503
|
+
return opts.credentials.server;
|
|
504
|
+
}
|
|
505
|
+
return DEFAULT_SERVER_URL;
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
// src/lib/auth/authorize-url.ts
|
|
509
|
+
function buildBrowserAuthorizeUrl(o) {
|
|
510
|
+
const params = new URLSearchParams();
|
|
511
|
+
params.set("client_id", o.clientId);
|
|
512
|
+
params.set("merchant_id", o.merchantId ?? RATIO_CLI_MERCHANT_ID);
|
|
513
|
+
params.set("code_challenge", o.codeChallenge);
|
|
514
|
+
params.set("code_challenge_method", "S256");
|
|
515
|
+
params.set("state", o.state);
|
|
516
|
+
params.set("redirect_uri", o.redirectUri);
|
|
517
|
+
if (o.scopes && o.scopes.length > 0) {
|
|
518
|
+
params.set("scope", o.scopes.join(","));
|
|
519
|
+
}
|
|
520
|
+
return `${o.serverUrl}/oauth/authorize?${params.toString()}`;
|
|
521
|
+
}
|
|
522
|
+
function buildManualAuthorizeUrl(o) {
|
|
523
|
+
const params = new URLSearchParams();
|
|
524
|
+
params.set("client_id", o.clientId);
|
|
525
|
+
params.set("merchant_id", o.merchantId ?? RATIO_CLI_MERCHANT_ID);
|
|
526
|
+
params.set("code_challenge", o.codeChallenge);
|
|
527
|
+
params.set("code_challenge_method", "S256");
|
|
528
|
+
params.set("state", o.state);
|
|
529
|
+
params.set("display_code", "1");
|
|
530
|
+
if (o.scopes && o.scopes.length > 0) {
|
|
531
|
+
params.set("scope", o.scopes.join(","));
|
|
532
|
+
}
|
|
533
|
+
return `${o.serverUrl}/oauth/authorize?${params.toString()}`;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// src/lib/auth/authorize-code-exchange.ts
|
|
537
|
+
import { z as z2 } from "zod";
|
|
538
|
+
|
|
539
|
+
// src/lib/auth/verbose-trace.ts
|
|
540
|
+
var noopTracer = () => {
|
|
541
|
+
};
|
|
542
|
+
function createVerboseTracer(verbose, write = (line) => {
|
|
543
|
+
process.stderr.write(line);
|
|
544
|
+
}) {
|
|
545
|
+
if (!verbose) return noopTracer;
|
|
546
|
+
return (msg) => {
|
|
547
|
+
write(`[verbose] ${msg}
|
|
548
|
+
`);
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// src/lib/auth/authorize-code-exchange.ts
|
|
553
|
+
var TokenResponseSchema = z2.object({
|
|
554
|
+
access_token: z2.string().min(1),
|
|
555
|
+
refresh_token: z2.string().min(1),
|
|
556
|
+
expires_in: z2.number().int().positive(),
|
|
557
|
+
token_type: z2.string().optional(),
|
|
558
|
+
scope: z2.string().optional(),
|
|
559
|
+
merchant_id: z2.string().optional()
|
|
560
|
+
});
|
|
561
|
+
function exchangeCodeForTokens(i) {
|
|
562
|
+
return _exchangeCodeForTokensImpl(i);
|
|
563
|
+
}
|
|
564
|
+
async function _exchangeCodeForTokensImpl(i) {
|
|
565
|
+
const fetchFn = i.fetchImpl ?? fetch;
|
|
566
|
+
const body = {
|
|
567
|
+
grant_type: "authorization_code",
|
|
568
|
+
code: i.code,
|
|
569
|
+
clientId: i.clientId,
|
|
570
|
+
codeVerifier: i.codeVerifier
|
|
571
|
+
};
|
|
572
|
+
if (i.redirectUri !== void 0) {
|
|
573
|
+
body["redirectUri"] = i.redirectUri;
|
|
574
|
+
}
|
|
575
|
+
const trace = i.trace ?? noopTracer;
|
|
576
|
+
const startedAt = Date.now();
|
|
577
|
+
let response;
|
|
578
|
+
try {
|
|
579
|
+
response = await fetchFn(`${i.serverUrl}/oauth/token`, {
|
|
580
|
+
method: "POST",
|
|
581
|
+
headers: { "Content-Type": "application/json" },
|
|
582
|
+
body: JSON.stringify(body)
|
|
583
|
+
});
|
|
584
|
+
} catch {
|
|
585
|
+
trace(`POST /oauth/token \u2192 network error (${Date.now() - startedAt}ms)`);
|
|
586
|
+
throw new NetworkError("Failed to reach the Ratio server during token exchange.");
|
|
587
|
+
}
|
|
588
|
+
trace(`POST /oauth/token \u2192 ${response.status} (${Date.now() - startedAt}ms)`);
|
|
589
|
+
if (response.status >= 400 && response.status < 500) {
|
|
590
|
+
throw new ValidationError(
|
|
591
|
+
"The authorization code was rejected. Run 'ratio auth login' again."
|
|
592
|
+
);
|
|
593
|
+
}
|
|
594
|
+
if (response.status >= 500 || !response.ok) {
|
|
595
|
+
throw new NetworkError("The Ratio server returned an error during token exchange.");
|
|
596
|
+
}
|
|
597
|
+
let raw;
|
|
598
|
+
try {
|
|
599
|
+
raw = await response.json();
|
|
600
|
+
} catch {
|
|
601
|
+
throw new ValidationError("Token response was not valid JSON.");
|
|
602
|
+
}
|
|
603
|
+
const parsed = TokenResponseSchema.safeParse(raw);
|
|
604
|
+
if (!parsed.success) {
|
|
605
|
+
throw new ValidationError("Token response shape was unexpected. Run ratio auth login again.");
|
|
606
|
+
}
|
|
607
|
+
const data = parsed.data;
|
|
608
|
+
const expiresAt = new Date(Date.now() + data.expires_in * 1e3).toISOString();
|
|
609
|
+
const tokens = {
|
|
610
|
+
accessToken: data.access_token,
|
|
611
|
+
refreshToken: data.refresh_token,
|
|
612
|
+
expiresAt
|
|
613
|
+
};
|
|
614
|
+
const scopeStr = data.scope ?? "";
|
|
615
|
+
const scopes = scopeStr.length > 0 ? scopeStr.split(/[\s,]+/).filter(Boolean) : [];
|
|
616
|
+
const identity = {
|
|
617
|
+
clientId: i.clientId,
|
|
618
|
+
merchantId: data.merchant_id ?? "ratio-cli-dev",
|
|
619
|
+
scopes
|
|
620
|
+
};
|
|
621
|
+
return { tokens, identity };
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// src/lib/auth/http-refresher.ts
|
|
625
|
+
import { z as z3 } from "zod";
|
|
626
|
+
var RefreshResponseSchema = z3.object({
|
|
627
|
+
access_token: z3.string().min(1),
|
|
628
|
+
refresh_token: z3.string().min(1),
|
|
629
|
+
expires_in: z3.number().int().positive()
|
|
630
|
+
});
|
|
631
|
+
var HttpTokenRefresher = class {
|
|
632
|
+
serverUrl;
|
|
633
|
+
clientId;
|
|
634
|
+
fetchImpl;
|
|
635
|
+
trace;
|
|
636
|
+
constructor(opts) {
|
|
637
|
+
this.serverUrl = opts.serverUrl;
|
|
638
|
+
this.clientId = opts.clientId;
|
|
639
|
+
this.fetchImpl = opts.fetchImpl ?? fetch;
|
|
640
|
+
this.trace = opts.trace ?? noopTracer;
|
|
641
|
+
}
|
|
642
|
+
async refresh(refreshToken) {
|
|
643
|
+
const body = {
|
|
644
|
+
grant_type: "refresh_token",
|
|
645
|
+
refresh_token: refreshToken,
|
|
646
|
+
clientId: this.clientId
|
|
647
|
+
};
|
|
648
|
+
this.trace("refresh: fired (access token expired or expiring)");
|
|
649
|
+
const startedAt = Date.now();
|
|
650
|
+
let response;
|
|
651
|
+
try {
|
|
652
|
+
response = await this.fetchImpl(`${this.serverUrl}/oauth/token`, {
|
|
653
|
+
method: "POST",
|
|
654
|
+
headers: { "Content-Type": "application/json" },
|
|
655
|
+
body: JSON.stringify(body)
|
|
656
|
+
});
|
|
657
|
+
} catch {
|
|
658
|
+
this.trace(`POST /oauth/token \u2192 network error (${Date.now() - startedAt}ms)`);
|
|
659
|
+
throw new NetworkError("Failed to reach the Ratio server during token refresh.");
|
|
660
|
+
}
|
|
661
|
+
this.trace(`POST /oauth/token \u2192 ${response.status} (${Date.now() - startedAt}ms)`);
|
|
662
|
+
if (response.status >= 400 && response.status < 500) {
|
|
663
|
+
throw new TokenExpiredError(
|
|
664
|
+
"Your session has expired and could not be refreshed."
|
|
665
|
+
);
|
|
666
|
+
}
|
|
667
|
+
if (response.status >= 500 || !response.ok) {
|
|
668
|
+
throw new NetworkError("The Ratio server returned an error during token refresh.");
|
|
669
|
+
}
|
|
670
|
+
let raw;
|
|
671
|
+
try {
|
|
672
|
+
raw = await response.json();
|
|
673
|
+
} catch {
|
|
674
|
+
throw new NetworkError("Token refresh response was not valid JSON.");
|
|
675
|
+
}
|
|
676
|
+
const parsed = RefreshResponseSchema.safeParse(raw);
|
|
677
|
+
if (!parsed.success) {
|
|
678
|
+
throw new TokenExpiredError(
|
|
679
|
+
"Your session has expired and could not be refreshed."
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
const data = parsed.data;
|
|
683
|
+
const expiresAt = new Date(Date.now() + data.expires_in * 1e3).toISOString();
|
|
684
|
+
return {
|
|
685
|
+
accessToken: data.access_token,
|
|
686
|
+
refreshToken: data.refresh_token,
|
|
687
|
+
expiresAt
|
|
688
|
+
};
|
|
689
|
+
}
|
|
690
|
+
};
|
|
691
|
+
|
|
692
|
+
// src/lib/auth/loopback-server.ts
|
|
693
|
+
import http from "http";
|
|
694
|
+
function buildLoopbackRedirectUri(port) {
|
|
695
|
+
return `http://127.0.0.1:${port}/callback`;
|
|
696
|
+
}
|
|
697
|
+
function startLoopbackSession(trace = noopTracer) {
|
|
698
|
+
return new Promise((resolve, reject) => {
|
|
699
|
+
let settled = false;
|
|
700
|
+
function settle(fn) {
|
|
701
|
+
if (settled) return;
|
|
702
|
+
settled = true;
|
|
703
|
+
fn();
|
|
704
|
+
}
|
|
705
|
+
const server = http.createServer();
|
|
706
|
+
server.on("error", reject);
|
|
707
|
+
server.listen(0, "127.0.0.1", () => {
|
|
708
|
+
const addr = server.address();
|
|
709
|
+
const port = typeof addr === "object" && addr !== null ? addr.port : 0;
|
|
710
|
+
const redirectUri = buildLoopbackRedirectUri(port);
|
|
711
|
+
trace(`loopback: bound 127.0.0.1:${port}`);
|
|
712
|
+
resolve({
|
|
713
|
+
port,
|
|
714
|
+
redirectUri,
|
|
715
|
+
awaitCallback(opts) {
|
|
716
|
+
const timeoutMs = opts.timeoutMs ?? 3e4;
|
|
717
|
+
return new Promise((res, rej) => {
|
|
718
|
+
server.on("request", (req, response) => {
|
|
719
|
+
const reqUrl = new URL(req.url ?? "/", `http://127.0.0.1`);
|
|
720
|
+
if (reqUrl.pathname !== "/callback") {
|
|
721
|
+
response.writeHead(404, { "Content-Type": "text/plain" });
|
|
722
|
+
response.end("Not found");
|
|
723
|
+
return;
|
|
724
|
+
}
|
|
725
|
+
const code = reqUrl.searchParams.get("code");
|
|
726
|
+
const state = reqUrl.searchParams.get("state");
|
|
727
|
+
if (!state || !timingSafeEqualState(state, opts.expectedState)) {
|
|
728
|
+
trace("callback: state mismatch");
|
|
729
|
+
response.writeHead(400, { "Content-Type": "text/plain" });
|
|
730
|
+
response.end("State mismatch");
|
|
731
|
+
settle(() => {
|
|
732
|
+
clearTimeout(timer);
|
|
733
|
+
server.close(
|
|
734
|
+
() => rej(new ValidationError("Authorization callback failed a security check."))
|
|
735
|
+
);
|
|
736
|
+
});
|
|
737
|
+
return;
|
|
738
|
+
}
|
|
739
|
+
if (!code || code.length === 0) {
|
|
740
|
+
trace("callback: missing code");
|
|
741
|
+
response.writeHead(400, { "Content-Type": "text/plain" });
|
|
742
|
+
response.end("Missing code");
|
|
743
|
+
settle(() => {
|
|
744
|
+
clearTimeout(timer);
|
|
745
|
+
server.close(
|
|
746
|
+
() => rej(new ValidationError("Authorization callback did not return a code."))
|
|
747
|
+
);
|
|
748
|
+
});
|
|
749
|
+
return;
|
|
750
|
+
}
|
|
751
|
+
response.writeHead(200, { "Content-Type": "text/html" });
|
|
752
|
+
response.end(
|
|
753
|
+
"<html><body><h1>Signed in! You can close this tab.</h1></body></html>"
|
|
754
|
+
);
|
|
755
|
+
trace(`callback: received on port ${port} (state ok)`);
|
|
756
|
+
settle(() => {
|
|
757
|
+
clearTimeout(timer);
|
|
758
|
+
server.close(() => res({ code, redirectUri }));
|
|
759
|
+
});
|
|
760
|
+
});
|
|
761
|
+
const timer = setTimeout(() => {
|
|
762
|
+
trace(`callback: timeout fired after ${timeoutMs}ms`);
|
|
763
|
+
settle(() => {
|
|
764
|
+
server.close(
|
|
765
|
+
() => rej(
|
|
766
|
+
new NotLoggedInError(
|
|
767
|
+
"Browser flow timed out. Run 'ratio auth login --manual' to complete without a browser."
|
|
768
|
+
)
|
|
769
|
+
)
|
|
770
|
+
);
|
|
771
|
+
});
|
|
772
|
+
}, timeoutMs);
|
|
773
|
+
if (typeof timer.unref === "function") {
|
|
774
|
+
timer.unref();
|
|
775
|
+
}
|
|
776
|
+
if (opts.signal) {
|
|
777
|
+
opts.signal.addEventListener("abort", () => {
|
|
778
|
+
trace("callback: cancelled");
|
|
779
|
+
clearTimeout(timer);
|
|
780
|
+
settle(() => {
|
|
781
|
+
server.close(
|
|
782
|
+
() => rej(
|
|
783
|
+
new NotLoggedInError(
|
|
784
|
+
"Browser flow was cancelled. Run 'ratio auth login --manual' to complete without a browser."
|
|
785
|
+
)
|
|
786
|
+
)
|
|
787
|
+
);
|
|
788
|
+
});
|
|
789
|
+
});
|
|
790
|
+
}
|
|
791
|
+
});
|
|
792
|
+
}
|
|
793
|
+
});
|
|
794
|
+
});
|
|
795
|
+
});
|
|
796
|
+
}
|
|
797
|
+
|
|
798
|
+
// src/lib/auth/browser.ts
|
|
799
|
+
function defaultBrowserOpener() {
|
|
800
|
+
return {
|
|
801
|
+
async open(url) {
|
|
802
|
+
let openFn;
|
|
803
|
+
try {
|
|
804
|
+
const mod = await import("open");
|
|
805
|
+
openFn = mod.default;
|
|
806
|
+
} catch {
|
|
807
|
+
throw new NotLoggedInError(
|
|
808
|
+
"Could not open a browser. Run 'ratio auth login --manual' to complete without a browser."
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
try {
|
|
812
|
+
await openFn(url);
|
|
813
|
+
} catch {
|
|
814
|
+
throw new NotLoggedInError(
|
|
815
|
+
"Could not open a browser. Run 'ratio auth login --manual' to complete without a browser."
|
|
816
|
+
);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// src/lib/auth/manual-prompt.ts
|
|
823
|
+
import { createInterface } from "readline/promises";
|
|
824
|
+
import { stdin as input, stdout as output } from "process";
|
|
825
|
+
function defaultStdinPrompt() {
|
|
826
|
+
return {
|
|
827
|
+
async askForCode(prompt) {
|
|
828
|
+
const rl = createInterface({ input, output });
|
|
829
|
+
try {
|
|
830
|
+
const answer = await rl.question(prompt);
|
|
831
|
+
return answer.trim();
|
|
832
|
+
} finally {
|
|
833
|
+
rl.close();
|
|
834
|
+
}
|
|
835
|
+
}
|
|
836
|
+
};
|
|
837
|
+
}
|
|
838
|
+
|
|
839
|
+
// src/commands/auth/login.ts
|
|
840
|
+
var AuthLogin = class _AuthLogin extends RatioCommand {
|
|
841
|
+
static description = "Sign in to Ratio.";
|
|
842
|
+
static examples = [
|
|
843
|
+
"<%= config.bin %> auth login",
|
|
844
|
+
"<%= config.bin %> auth login --manual"
|
|
845
|
+
];
|
|
846
|
+
static args = {};
|
|
847
|
+
static flags = {
|
|
848
|
+
...verboseFlag,
|
|
849
|
+
manual: Flags2.boolean({
|
|
850
|
+
description: "Use manual copy-paste flow instead of a browser."
|
|
851
|
+
}),
|
|
852
|
+
profile: Flags2.string({
|
|
853
|
+
description: "Credential profile name to write into.",
|
|
854
|
+
default: "default"
|
|
855
|
+
})
|
|
856
|
+
};
|
|
857
|
+
// Injectable seams (test doubles replace these).
|
|
858
|
+
browserOpener = defaultBrowserOpener();
|
|
859
|
+
stdinPrompt = defaultStdinPrompt();
|
|
860
|
+
spinner = ux.action;
|
|
861
|
+
/** Verbose diagnostic tracer — no-op unless --verbose; set in run(). */
|
|
862
|
+
trace = noopTracer;
|
|
863
|
+
async run() {
|
|
864
|
+
const { flags } = await this.parse(_AuthLogin);
|
|
865
|
+
if (flags.verbose) {
|
|
866
|
+
this.trace = createVerboseTracer(true);
|
|
867
|
+
}
|
|
868
|
+
const envToken = process.env["RATIO_API_TOKEN"];
|
|
869
|
+
if (typeof envToken === "string" && envToken.length > 0) {
|
|
870
|
+
this.log("Using RATIO_API_TOKEN \u2014 no login needed.");
|
|
871
|
+
return;
|
|
872
|
+
}
|
|
873
|
+
const serverUrl = resolveServerUrl();
|
|
874
|
+
const { codeVerifier, codeChallenge } = generatePkcePair();
|
|
875
|
+
const state = generateState();
|
|
876
|
+
const profileName = flags.profile ?? "default";
|
|
877
|
+
const refresher = new HttpTokenRefresher({
|
|
878
|
+
serverUrl,
|
|
879
|
+
clientId: RATIO_CLI_CLIENT_ID,
|
|
880
|
+
trace: this.trace
|
|
881
|
+
});
|
|
882
|
+
const store = new FileCredentialStore({ refresher });
|
|
883
|
+
if (flags.manual) {
|
|
884
|
+
await this._runManualFlow(serverUrl, codeVerifier, codeChallenge, state, store, profileName);
|
|
885
|
+
} else {
|
|
886
|
+
await this._runBrowserFlow(serverUrl, codeVerifier, codeChallenge, state, store, profileName);
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
async _runBrowserFlow(serverUrl, codeVerifier, codeChallenge, state, store, profileName) {
|
|
890
|
+
const session = await startLoopbackSession(this.trace);
|
|
891
|
+
const authorizeUrl = buildBrowserAuthorizeUrl({
|
|
892
|
+
serverUrl,
|
|
893
|
+
clientId: RATIO_CLI_CLIENT_ID,
|
|
894
|
+
codeChallenge,
|
|
895
|
+
state,
|
|
896
|
+
redirectUri: session.redirectUri
|
|
897
|
+
});
|
|
898
|
+
await this.browserOpener.open(authorizeUrl);
|
|
899
|
+
this.log("Opening your browser for Ratio sign-in...");
|
|
900
|
+
this.spinner.start("Waiting for browser sign-in");
|
|
901
|
+
let code;
|
|
902
|
+
let redirectUri;
|
|
903
|
+
try {
|
|
904
|
+
({ code, redirectUri } = await session.awaitCallback({
|
|
905
|
+
expectedState: state,
|
|
906
|
+
timeoutMs: 3e4
|
|
907
|
+
}));
|
|
908
|
+
} finally {
|
|
909
|
+
this.spinner.stop();
|
|
910
|
+
}
|
|
911
|
+
const { tokens, identity } = await exchangeCodeForTokens({
|
|
912
|
+
serverUrl,
|
|
913
|
+
code,
|
|
914
|
+
codeVerifier,
|
|
915
|
+
redirectUri,
|
|
916
|
+
clientId: RATIO_CLI_CLIENT_ID,
|
|
917
|
+
trace: this.trace
|
|
918
|
+
});
|
|
919
|
+
await this._persistCredentials(store, serverUrl, profileName, tokens, identity);
|
|
920
|
+
this.log(`Logged in as clientId=${identity.clientId} merchantId=${identity.merchantId}`);
|
|
921
|
+
}
|
|
922
|
+
async _runManualFlow(serverUrl, codeVerifier, codeChallenge, state, store, profileName) {
|
|
923
|
+
const authorizeUrl = buildManualAuthorizeUrl({
|
|
924
|
+
serverUrl,
|
|
925
|
+
clientId: RATIO_CLI_CLIENT_ID,
|
|
926
|
+
codeChallenge,
|
|
927
|
+
state
|
|
928
|
+
});
|
|
929
|
+
this.log("Open this URL in your browser to sign in:");
|
|
930
|
+
this.log(authorizeUrl);
|
|
931
|
+
this.log("");
|
|
932
|
+
const code = await this.stdinPrompt.askForCode("Paste the code shown in the browser: ");
|
|
933
|
+
if (!code || code.length === 0) {
|
|
934
|
+
throw new ValidationError(
|
|
935
|
+
"The pasted code was empty or malformed. Try again."
|
|
936
|
+
);
|
|
937
|
+
}
|
|
938
|
+
const { tokens, identity } = await exchangeCodeForTokens({
|
|
939
|
+
serverUrl,
|
|
940
|
+
code,
|
|
941
|
+
codeVerifier,
|
|
942
|
+
clientId: RATIO_CLI_CLIENT_ID,
|
|
943
|
+
trace: this.trace
|
|
944
|
+
// redirectUri intentionally omitted for manual flow
|
|
945
|
+
});
|
|
946
|
+
await this._persistCredentials(store, serverUrl, profileName, tokens, identity);
|
|
947
|
+
this.log(`Logged in as clientId=${identity.clientId} merchantId=${identity.merchantId}`);
|
|
948
|
+
}
|
|
949
|
+
async _persistCredentials(store, serverUrl, profileName, tokens, identity) {
|
|
950
|
+
let existingCreds;
|
|
951
|
+
try {
|
|
952
|
+
existingCreds = await store.load();
|
|
953
|
+
} catch {
|
|
954
|
+
existingCreds = {
|
|
955
|
+
version: 1,
|
|
956
|
+
server: serverUrl,
|
|
957
|
+
clientId: RATIO_CLI_CLIENT_ID,
|
|
958
|
+
profiles: {}
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
const updated = {
|
|
962
|
+
...existingCreds,
|
|
963
|
+
server: serverUrl,
|
|
964
|
+
clientId: RATIO_CLI_CLIENT_ID,
|
|
965
|
+
profiles: {
|
|
966
|
+
...existingCreds.profiles,
|
|
967
|
+
[profileName]: {
|
|
968
|
+
accessToken: tokens.accessToken,
|
|
969
|
+
refreshToken: tokens.refreshToken,
|
|
970
|
+
expiresAt: tokens.expiresAt,
|
|
971
|
+
identity: {
|
|
972
|
+
developerId: identity.clientId,
|
|
973
|
+
email: "",
|
|
974
|
+
merchantId: identity.merchantId,
|
|
975
|
+
scopes: [...identity.scopes]
|
|
976
|
+
},
|
|
977
|
+
obtainedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
978
|
+
}
|
|
979
|
+
}
|
|
980
|
+
};
|
|
981
|
+
await store.save(updated);
|
|
982
|
+
}
|
|
983
|
+
};
|
|
984
|
+
export {
|
|
985
|
+
AuthLogin as default
|
|
986
|
+
};
|
|
987
|
+
//# sourceMappingURL=login.js.map
|