@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,138 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { Credentials } from './types.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Runtime validation for the on-disk credentials file.
|
|
6
|
+
*
|
|
7
|
+
* The parser is deliberately strict: unknown top-level keys, missing
|
|
8
|
+
* fields, non-ISO timestamps, and any `version` other than `1` are all
|
|
9
|
+
* rejected with a typed ValidationError. Error messages NEVER embed the
|
|
10
|
+
* raw file contents — a malformed file may contain a truncated token.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/** Strict zod schema for the version-1 credentials file. */
|
|
14
|
+
declare const credentialsSchema: z.ZodObject<{
|
|
15
|
+
version: z.ZodLiteral<1>;
|
|
16
|
+
server: z.ZodString;
|
|
17
|
+
clientId: z.ZodString;
|
|
18
|
+
profiles: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
19
|
+
accessToken: z.ZodString;
|
|
20
|
+
refreshToken: z.ZodString;
|
|
21
|
+
expiresAt: z.ZodString;
|
|
22
|
+
identity: z.ZodObject<{
|
|
23
|
+
developerId: z.ZodString;
|
|
24
|
+
email: z.ZodString;
|
|
25
|
+
merchantId: z.ZodOptional<z.ZodString>;
|
|
26
|
+
scopes: z.ZodOptional<z.ZodArray<z.ZodString, "many">>;
|
|
27
|
+
}, "strict", z.ZodTypeAny, {
|
|
28
|
+
developerId: string;
|
|
29
|
+
email: string;
|
|
30
|
+
merchantId?: string | undefined;
|
|
31
|
+
scopes?: string[] | undefined;
|
|
32
|
+
}, {
|
|
33
|
+
developerId: string;
|
|
34
|
+
email: string;
|
|
35
|
+
merchantId?: string | undefined;
|
|
36
|
+
scopes?: string[] | undefined;
|
|
37
|
+
}>;
|
|
38
|
+
obtainedAt: z.ZodString;
|
|
39
|
+
}, "strict", z.ZodTypeAny, {
|
|
40
|
+
accessToken: string;
|
|
41
|
+
refreshToken: string;
|
|
42
|
+
expiresAt: string;
|
|
43
|
+
identity: {
|
|
44
|
+
developerId: string;
|
|
45
|
+
email: string;
|
|
46
|
+
merchantId?: string | undefined;
|
|
47
|
+
scopes?: string[] | undefined;
|
|
48
|
+
};
|
|
49
|
+
obtainedAt: string;
|
|
50
|
+
}, {
|
|
51
|
+
accessToken: string;
|
|
52
|
+
refreshToken: string;
|
|
53
|
+
expiresAt: string;
|
|
54
|
+
identity: {
|
|
55
|
+
developerId: string;
|
|
56
|
+
email: string;
|
|
57
|
+
merchantId?: string | undefined;
|
|
58
|
+
scopes?: string[] | undefined;
|
|
59
|
+
};
|
|
60
|
+
obtainedAt: string;
|
|
61
|
+
}>>;
|
|
62
|
+
developer: z.ZodOptional<z.ZodObject<{
|
|
63
|
+
jwt: z.ZodString;
|
|
64
|
+
developerId: z.ZodString;
|
|
65
|
+
email: z.ZodString;
|
|
66
|
+
expiresAt: z.ZodString;
|
|
67
|
+
obtainedAt: z.ZodString;
|
|
68
|
+
}, "strict", z.ZodTypeAny, {
|
|
69
|
+
developerId: string;
|
|
70
|
+
email: string;
|
|
71
|
+
expiresAt: string;
|
|
72
|
+
obtainedAt: string;
|
|
73
|
+
jwt: string;
|
|
74
|
+
}, {
|
|
75
|
+
developerId: string;
|
|
76
|
+
email: string;
|
|
77
|
+
expiresAt: string;
|
|
78
|
+
obtainedAt: string;
|
|
79
|
+
jwt: string;
|
|
80
|
+
}>>;
|
|
81
|
+
}, "strict", z.ZodTypeAny, {
|
|
82
|
+
version: 1;
|
|
83
|
+
server: string;
|
|
84
|
+
clientId: string;
|
|
85
|
+
profiles: Record<string, {
|
|
86
|
+
accessToken: string;
|
|
87
|
+
refreshToken: string;
|
|
88
|
+
expiresAt: string;
|
|
89
|
+
identity: {
|
|
90
|
+
developerId: string;
|
|
91
|
+
email: string;
|
|
92
|
+
merchantId?: string | undefined;
|
|
93
|
+
scopes?: string[] | undefined;
|
|
94
|
+
};
|
|
95
|
+
obtainedAt: string;
|
|
96
|
+
}>;
|
|
97
|
+
developer?: {
|
|
98
|
+
developerId: string;
|
|
99
|
+
email: string;
|
|
100
|
+
expiresAt: string;
|
|
101
|
+
obtainedAt: string;
|
|
102
|
+
jwt: string;
|
|
103
|
+
} | undefined;
|
|
104
|
+
}, {
|
|
105
|
+
version: 1;
|
|
106
|
+
server: string;
|
|
107
|
+
clientId: string;
|
|
108
|
+
profiles: Record<string, {
|
|
109
|
+
accessToken: string;
|
|
110
|
+
refreshToken: string;
|
|
111
|
+
expiresAt: string;
|
|
112
|
+
identity: {
|
|
113
|
+
developerId: string;
|
|
114
|
+
email: string;
|
|
115
|
+
merchantId?: string | undefined;
|
|
116
|
+
scopes?: string[] | undefined;
|
|
117
|
+
};
|
|
118
|
+
obtainedAt: string;
|
|
119
|
+
}>;
|
|
120
|
+
developer?: {
|
|
121
|
+
developerId: string;
|
|
122
|
+
email: string;
|
|
123
|
+
expiresAt: string;
|
|
124
|
+
obtainedAt: string;
|
|
125
|
+
jwt: string;
|
|
126
|
+
} | undefined;
|
|
127
|
+
}>;
|
|
128
|
+
/**
|
|
129
|
+
* Validate an already-JSON-parsed value against the version-1 shape.
|
|
130
|
+
*
|
|
131
|
+
* Throws ValidationError on any mismatch. A recognisable-but-unsupported
|
|
132
|
+
* `version` gets a dedicated upgrade message; the version label is only
|
|
133
|
+
* echoed when it is a number so arbitrary file contents can never leak
|
|
134
|
+
* into an error message.
|
|
135
|
+
*/
|
|
136
|
+
declare function parseCredentials(input: unknown): Credentials;
|
|
137
|
+
|
|
138
|
+
export { credentialsSchema, parseCredentials };
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
// src/lib/credentials/schema.ts
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
|
|
4
|
+
// src/lib/errors.ts
|
|
5
|
+
var RatioCLIError = class extends Error {
|
|
6
|
+
code;
|
|
7
|
+
exitCode;
|
|
8
|
+
hint;
|
|
9
|
+
docsUrl;
|
|
10
|
+
constructor(opts) {
|
|
11
|
+
super(opts.message);
|
|
12
|
+
this.name = new.target.name;
|
|
13
|
+
this.code = opts.code;
|
|
14
|
+
this.exitCode = opts.exitCode;
|
|
15
|
+
if (opts.hint !== void 0) this.hint = opts.hint;
|
|
16
|
+
if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var ValidationError = class extends RatioCLIError {
|
|
20
|
+
constructor(message, hint) {
|
|
21
|
+
const opts = {
|
|
22
|
+
code: "VALIDATION_ERROR",
|
|
23
|
+
exitCode: 8,
|
|
24
|
+
message
|
|
25
|
+
};
|
|
26
|
+
if (hint !== void 0) opts.hint = hint;
|
|
27
|
+
super(opts);
|
|
28
|
+
}
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// src/lib/credentials/schema.ts
|
|
32
|
+
var identitySchema = z.object({
|
|
33
|
+
developerId: z.string(),
|
|
34
|
+
email: z.string(),
|
|
35
|
+
// Token-derived fields captured at login. Optional (additive) so files
|
|
36
|
+
// written before these fields existed still validate as version 1.
|
|
37
|
+
merchantId: z.string().optional(),
|
|
38
|
+
scopes: z.array(z.string()).optional()
|
|
39
|
+
}).strict();
|
|
40
|
+
var profileSchema = z.object({
|
|
41
|
+
accessToken: z.string(),
|
|
42
|
+
refreshToken: z.string(),
|
|
43
|
+
expiresAt: z.string().datetime({ offset: true }),
|
|
44
|
+
identity: identitySchema,
|
|
45
|
+
obtainedAt: z.string().datetime({ offset: true })
|
|
46
|
+
}).strict();
|
|
47
|
+
var developerSessionSchema = z.object({
|
|
48
|
+
jwt: z.string(),
|
|
49
|
+
developerId: z.string(),
|
|
50
|
+
email: z.string(),
|
|
51
|
+
expiresAt: z.string().datetime({ offset: true }),
|
|
52
|
+
obtainedAt: z.string().datetime({ offset: true })
|
|
53
|
+
}).strict();
|
|
54
|
+
var credentialsSchema = z.object({
|
|
55
|
+
version: z.literal(1),
|
|
56
|
+
server: z.string(),
|
|
57
|
+
clientId: z.string(),
|
|
58
|
+
profiles: z.record(z.string(), profileSchema),
|
|
59
|
+
// Optional cached developer session. Additive so files written before
|
|
60
|
+
// this field existed still validate as version 1.
|
|
61
|
+
developer: developerSessionSchema.optional()
|
|
62
|
+
}).strict();
|
|
63
|
+
function parseCredentials(input) {
|
|
64
|
+
if (typeof input === "object" && input !== null && "version" in input) {
|
|
65
|
+
const version = input.version;
|
|
66
|
+
if (version !== 1) {
|
|
67
|
+
const label = typeof version === "number" ? String(version) : "unknown";
|
|
68
|
+
throw new ValidationError(
|
|
69
|
+
`credentials file version ${label} is unsupported by this CLI \u2014 upgrade the CLI or sign in again`
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
const result = credentialsSchema.safeParse(input);
|
|
74
|
+
if (!result.success) {
|
|
75
|
+
throw new ValidationError(
|
|
76
|
+
"credentials file schema mismatch \u2014 expected v1 shape"
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
return result.data;
|
|
80
|
+
}
|
|
81
|
+
export {
|
|
82
|
+
credentialsSchema,
|
|
83
|
+
parseCredentials
|
|
84
|
+
};
|
|
85
|
+
//# sourceMappingURL=schema.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../src/lib/credentials/schema.ts","../../../src/lib/errors.ts"],"sourcesContent":["/**\n * Runtime validation for the on-disk credentials file.\n *\n * The parser is deliberately strict: unknown top-level keys, missing\n * fields, non-ISO timestamps, and any `version` other than `1` are all\n * rejected with a typed ValidationError. Error messages NEVER embed the\n * raw file contents — a malformed file may contain a truncated token.\n */\n\nimport { z } from 'zod';\nimport { ValidationError } from '../errors.js';\nimport type { Credentials } from './types.js';\n\nconst identitySchema = z\n .object({\n developerId: z.string(),\n email: z.string(),\n // Token-derived fields captured at login. Optional (additive) so files\n // written before these fields existed still validate as version 1.\n merchantId: z.string().optional(),\n scopes: z.array(z.string()).optional(),\n })\n .strict();\n\nconst profileSchema = z\n .object({\n accessToken: z.string(),\n refreshToken: z.string(),\n expiresAt: z.string().datetime({ offset: true }),\n identity: identitySchema,\n obtainedAt: z.string().datetime({ offset: true }),\n })\n .strict();\n\nconst developerSessionSchema = z\n .object({\n jwt: z.string(),\n developerId: z.string(),\n email: z.string(),\n expiresAt: z.string().datetime({ offset: true }),\n obtainedAt: z.string().datetime({ offset: true }),\n })\n .strict();\n\n/** Strict zod schema for the version-1 credentials file. */\nexport const credentialsSchema = z\n .object({\n version: z.literal(1),\n server: z.string(),\n clientId: z.string(),\n profiles: z.record(z.string(), profileSchema),\n // Optional cached developer session. Additive so files written before\n // this field existed still validate as version 1.\n developer: developerSessionSchema.optional(),\n })\n .strict();\n\n/**\n * Validate an already-JSON-parsed value against the version-1 shape.\n *\n * Throws ValidationError on any mismatch. A recognisable-but-unsupported\n * `version` gets a dedicated upgrade message; the version label is only\n * echoed when it is a number so arbitrary file contents can never leak\n * into an error message.\n */\nexport function parseCredentials(input: unknown): Credentials {\n if (typeof input === 'object' && input !== null && 'version' in input) {\n const version = (input as { version: unknown }).version;\n if (version !== 1) {\n const label = typeof version === 'number' ? String(version) : 'unknown';\n throw new ValidationError(\n `credentials file version ${label} is unsupported by this CLI — upgrade the CLI or sign in again`,\n );\n }\n }\n\n const result = credentialsSchema.safeParse(input);\n if (!result.success) {\n throw new ValidationError(\n 'credentials file schema mismatch — expected v1 shape',\n );\n }\n return result.data as Credentials;\n}\n","/**\n * Error hierarchy for the Ratio CLI. Every error thrown from a command's\n * run() is expected to be an instance of RatioCLIError so the global\n * render-error handler can map it to a deterministic stderr line and exit\n * code. Uncaught Error subclasses render as \"INTERNAL: <msg>\" with exit 1.\n *\n * No import from @oclif/core — this file is dependency-free so tests\n * can import it without oclif overhead.\n */\n\nexport class RatioCLIError extends Error {\n readonly code: string;\n readonly exitCode: number;\n readonly hint?: string;\n readonly docsUrl?: string;\n\n constructor(opts: {\n code: string;\n exitCode: number;\n message: string;\n hint?: string;\n docsUrl?: string;\n }) {\n super(opts.message);\n this.name = new.target.name;\n this.code = opts.code;\n this.exitCode = opts.exitCode;\n if (opts.hint !== undefined) this.hint = opts.hint;\n if (opts.docsUrl !== undefined) this.docsUrl = opts.docsUrl;\n }\n}\n\n/** Coming-soon stub UX. Renders on stdout with exit 0. */\nexport class ComingSoonError extends RatioCLIError {\n constructor(message: string) {\n super({ code: 'COMING_SOON', exitCode: 0, message });\n }\n}\n\n/** User has no credentials and command requires auth. */\nexport class NotLoggedInError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to sign in.\",\n ) {\n super({ code: 'NOT_LOGGED_IN', exitCode: 3, message, hint });\n }\n}\n\nexport class TokenExpiredError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Run 'ratio auth login' to refresh your session.\",\n ) {\n super({ code: 'TOKEN_EXPIRED', exitCode: 3, message, hint });\n }\n}\n\nexport class ScopeDeniedError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'SCOPE_DENIED',\n exitCode: 4,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class NetworkError extends RatioCLIError {\n constructor(message: string, hint: string = 'Check your network and retry.') {\n super({ code: 'NETWORK_ERROR', exitCode: 5, message, hint });\n }\n}\n\nexport class RelayConnectError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Verify the tunnel URL and that the auth-server is reachable.',\n ) {\n super({ code: 'RELAY_CONNECT', exitCode: 6, message, hint });\n }\n}\n\nexport class TemplateExistsError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Choose a different name or remove the existing directory.',\n ) {\n super({ code: 'TEMPLATE_EXISTS', exitCode: 7, message, hint });\n }\n}\n\nexport class ValidationError extends RatioCLIError {\n constructor(message: string, hint?: string) {\n const opts: { code: string; exitCode: number; message: string; hint?: string } = {\n code: 'VALIDATION_ERROR',\n exitCode: 8,\n message,\n };\n if (hint !== undefined) opts.hint = hint;\n super(opts);\n }\n}\n\nexport class RelayResumeGapError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Reconnect from the current cursor; older events are outside the buffer window.',\n ) {\n super({ code: 'RELAY_RESUME_GAP', exitCode: 9, message, hint });\n }\n}\n\nexport class OrchestratorSpawnError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = \"Could not start your app's dev command — check that the command in ratio.config.jsonc runs from this directory.\",\n ) {\n super({ code: 'ORCHESTRATOR_SPAWN', exitCode: 10, message, hint });\n }\n}\n\nexport class OrchestratorDrainTimeoutError extends RatioCLIError {\n constructor(\n message: string,\n hint: string = 'Your dev command did not exit cleanly within 5 seconds — it was forcibly terminated.',\n ) {\n super({ code: 'ORCHESTRATOR_DRAIN', exitCode: 11, message, hint });\n }\n}\n\nexport function isRatioCLIError(err: unknown): err is RatioCLIError {\n return err instanceof RatioCLIError;\n}\n"],"mappings":";AASA,SAAS,SAAS;;;ACCX,IAAM,gBAAN,cAA4B,MAAM;AAAA,EAC9B;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EAET,YAAY,MAMT;AACD,UAAM,KAAK,OAAO;AAClB,SAAK,OAAO,WAAW;AACvB,SAAK,OAAO,KAAK;AACjB,SAAK,WAAW,KAAK;AACrB,QAAI,KAAK,SAAS,OAAW,MAAK,OAAO,KAAK;AAC9C,QAAI,KAAK,YAAY,OAAW,MAAK,UAAU,KAAK;AAAA,EACtD;AACF;AAgEO,IAAM,kBAAN,cAA8B,cAAc;AAAA,EACjD,YAAY,SAAiB,MAAe;AAC1C,UAAM,OAA2E;AAAA,MAC/E,MAAM;AAAA,MACN,UAAU;AAAA,MACV;AAAA,IACF;AACA,QAAI,SAAS,OAAW,MAAK,OAAO;AACpC,UAAM,IAAI;AAAA,EACZ;AACF;;;AD3FA,IAAM,iBAAiB,EACpB,OAAO;AAAA,EACN,aAAa,EAAE,OAAO;AAAA,EACtB,OAAO,EAAE,OAAO;AAAA;AAAA;AAAA,EAGhB,YAAY,EAAE,OAAO,EAAE,SAAS;AAAA,EAChC,QAAQ,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,SAAS;AACvC,CAAC,EACA,OAAO;AAEV,IAAM,gBAAgB,EACnB,OAAO;AAAA,EACN,aAAa,EAAE,OAAO;AAAA,EACtB,cAAc,EAAE,OAAO;AAAA,EACvB,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,KAAK,CAAC;AAAA,EAC/C,UAAU;AAAA,EACV,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,KAAK,CAAC;AAClD,CAAC,EACA,OAAO;AAEV,IAAM,yBAAyB,EAC5B,OAAO;AAAA,EACN,KAAK,EAAE,OAAO;AAAA,EACd,aAAa,EAAE,OAAO;AAAA,EACtB,OAAO,EAAE,OAAO;AAAA,EAChB,WAAW,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,KAAK,CAAC;AAAA,EAC/C,YAAY,EAAE,OAAO,EAAE,SAAS,EAAE,QAAQ,KAAK,CAAC;AAClD,CAAC,EACA,OAAO;AAGH,IAAM,oBAAoB,EAC9B,OAAO;AAAA,EACN,SAAS,EAAE,QAAQ,CAAC;AAAA,EACpB,QAAQ,EAAE,OAAO;AAAA,EACjB,UAAU,EAAE,OAAO;AAAA,EACnB,UAAU,EAAE,OAAO,EAAE,OAAO,GAAG,aAAa;AAAA;AAAA;AAAA,EAG5C,WAAW,uBAAuB,SAAS;AAC7C,CAAC,EACA,OAAO;AAUH,SAAS,iBAAiB,OAA6B;AAC5D,MAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,aAAa,OAAO;AACrE,UAAM,UAAW,MAA+B;AAChD,QAAI,YAAY,GAAG;AACjB,YAAM,QAAQ,OAAO,YAAY,WAAW,OAAO,OAAO,IAAI;AAC9D,YAAM,IAAI;AAAA,QACR,4BAA4B,KAAK;AAAA,MACnC;AAAA,IACF;AAAA,EACF;AAEA,QAAM,SAAS,kBAAkB,UAAU,KAAK;AAChD,MAAI,CAAC,OAAO,SAAS;AACnB,UAAM,IAAI;AAAA,MACR;AAAA,IACF;AAAA,EACF;AACA,SAAO,OAAO;AAChB;","names":[]}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { Clock } from './clock.js';
|
|
2
|
+
import { TokenRefresher } from './refresher.js';
|
|
3
|
+
import { Credentials, DeveloperSession } from './types.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* File-backed credential store.
|
|
7
|
+
*
|
|
8
|
+
* Token resolution precedence (highest first):
|
|
9
|
+
* 1. `RATIO_API_TOKEN` from the injected env reader — returned as-is
|
|
10
|
+
* with ZERO filesystem access and ZERO refresher involvement.
|
|
11
|
+
* 2. The requested profile (default: "default") from the credentials
|
|
12
|
+
* file, silently refreshed via the injected TokenRefresher when it
|
|
13
|
+
* is within 5 minutes of expiry.
|
|
14
|
+
*
|
|
15
|
+
* Write contract: atomic tmp + rename in the SAME directory, tmp created
|
|
16
|
+
* with mode 0600 at open time (POSIX), parent directory created 0700,
|
|
17
|
+
* belt-and-braces chmod 0600 after rename. On Windows no chmod / mode
|
|
18
|
+
* checks are ever performed. Concurrent writers are last-writer-wins by
|
|
19
|
+
* design — the atomic rename guarantees no half-written file, which is
|
|
20
|
+
* sufficient for a single-developer CLI.
|
|
21
|
+
*
|
|
22
|
+
* No secret (access token, refresh token, raw file contents) is ever
|
|
23
|
+
* included in an error message or a log line.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
interface CredentialStoreOptions {
|
|
27
|
+
/** Defaults to defaultCredentialsPath(). */
|
|
28
|
+
readonly filePath?: string;
|
|
29
|
+
/** Defaults to new SystemClock(). */
|
|
30
|
+
readonly clock?: Clock;
|
|
31
|
+
/** Defaults to new NotImplementedRefresher(). */
|
|
32
|
+
readonly refresher?: TokenRefresher;
|
|
33
|
+
/** Defaults to () => process.env — injected for env-precedence tests. */
|
|
34
|
+
readonly envReader?: () => Readonly<Record<string, string | undefined>>;
|
|
35
|
+
/** Defaults to process.platform. */
|
|
36
|
+
readonly platform?: NodeJS.Platform;
|
|
37
|
+
}
|
|
38
|
+
interface CredentialStore {
|
|
39
|
+
/** Reads the full credentials file. Throws NotLoggedInError if missing. */
|
|
40
|
+
load(): Promise<Credentials>;
|
|
41
|
+
/** Atomic write with 0600 on POSIX. tmp + rename in same dir. */
|
|
42
|
+
save(creds: Credentials): Promise<void>;
|
|
43
|
+
/** Deletes the credentials file. Idempotent. */
|
|
44
|
+
clear(): Promise<void>;
|
|
45
|
+
/**
|
|
46
|
+
* Env precedence: if envReader().RATIO_API_TOKEN is set, returns it
|
|
47
|
+
* WITHOUT reading the file, WITHOUT touching the refresher, WITHOUT
|
|
48
|
+
* any fs I/O. This is the auditable "no disk access" path.
|
|
49
|
+
*
|
|
50
|
+
* Otherwise: reads the file, validates, computes
|
|
51
|
+
* msUntilExpiry = new Date(profile.expiresAt).getTime() - clock.now()
|
|
52
|
+
* If msUntilExpiry < 300_000 (5 min), calls refresher.refresh(...),
|
|
53
|
+
* persists the new TokenSet atomically, returns the new accessToken.
|
|
54
|
+
* Otherwise returns profile.accessToken.
|
|
55
|
+
*/
|
|
56
|
+
getValidAccessToken(profileName?: string): Promise<string>;
|
|
57
|
+
/**
|
|
58
|
+
* Env precedence: RATIO_DEVELOPER_TOKEN if set (no fs I/O).
|
|
59
|
+
* Otherwise: load(); if creds.developer is absent or within 5 minutes of
|
|
60
|
+
* expiry, throws NotLoggedInError ('Developer signin required.').
|
|
61
|
+
* Otherwise returns creds.developer.jwt.
|
|
62
|
+
* NO refresh attempt — the caller drives the re-prompt UX.
|
|
63
|
+
*/
|
|
64
|
+
getValidDeveloperToken(): Promise<string>;
|
|
65
|
+
/**
|
|
66
|
+
* Merge a DeveloperSession into the loaded credentials (creating the file
|
|
67
|
+
* if absent), atomic tmp+rename, 0600.
|
|
68
|
+
*/
|
|
69
|
+
saveDeveloperSession(session: DeveloperSession): Promise<void>;
|
|
70
|
+
}
|
|
71
|
+
declare class FileCredentialStore implements CredentialStore {
|
|
72
|
+
private readonly filePath;
|
|
73
|
+
private readonly clock;
|
|
74
|
+
private readonly refresher;
|
|
75
|
+
private readonly envReader;
|
|
76
|
+
private readonly platform;
|
|
77
|
+
constructor(opts?: CredentialStoreOptions);
|
|
78
|
+
load(): Promise<Credentials>;
|
|
79
|
+
save(creds: Credentials): Promise<void>;
|
|
80
|
+
clear(): Promise<void>;
|
|
81
|
+
getValidAccessToken(profileName?: string): Promise<string>;
|
|
82
|
+
getValidDeveloperToken(): Promise<string>;
|
|
83
|
+
saveDeveloperSession(session: DeveloperSession): Promise<void>;
|
|
84
|
+
/**
|
|
85
|
+
* Exchange the profile's refresh token for a fresh token set, merge it
|
|
86
|
+
* into the loaded credentials (preserving every other profile), persist
|
|
87
|
+
* atomically, and return the new access token. A refresher failure is
|
|
88
|
+
* wrapped in TokenExpiredError; the underlying message is deliberately
|
|
89
|
+
* discarded so a secret can never leak through the error surface.
|
|
90
|
+
*/
|
|
91
|
+
private refreshAndPersist;
|
|
92
|
+
/**
|
|
93
|
+
* POSIX-only startup permission check: if the file exists with a mode
|
|
94
|
+
* other than 0600, re-apply 0600 and emit a warning to stderr. Never
|
|
95
|
+
* runs on Windows (stat modes there do not map to POSIX bits and chmod
|
|
96
|
+
* must not be called). Missing file is NOT an error here — load()
|
|
97
|
+
* reports it as NotLoggedInError.
|
|
98
|
+
*/
|
|
99
|
+
private reapplyFileModeIfDrifted;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
export { type CredentialStore, type CredentialStoreOptions, FileCredentialStore };
|
|
@@ -0,0 +1,337 @@
|
|
|
1
|
+
// src/lib/credentials/store.ts
|
|
2
|
+
import crypto from "crypto";
|
|
3
|
+
import fs from "fs";
|
|
4
|
+
import { dirname } from "path";
|
|
5
|
+
|
|
6
|
+
// src/lib/errors.ts
|
|
7
|
+
var RatioCLIError = class extends Error {
|
|
8
|
+
code;
|
|
9
|
+
exitCode;
|
|
10
|
+
hint;
|
|
11
|
+
docsUrl;
|
|
12
|
+
constructor(opts) {
|
|
13
|
+
super(opts.message);
|
|
14
|
+
this.name = new.target.name;
|
|
15
|
+
this.code = opts.code;
|
|
16
|
+
this.exitCode = opts.exitCode;
|
|
17
|
+
if (opts.hint !== void 0) this.hint = opts.hint;
|
|
18
|
+
if (opts.docsUrl !== void 0) this.docsUrl = opts.docsUrl;
|
|
19
|
+
}
|
|
20
|
+
};
|
|
21
|
+
var NotLoggedInError = class extends RatioCLIError {
|
|
22
|
+
constructor(message, hint = "Run 'ratio auth login' to sign in.") {
|
|
23
|
+
super({ code: "NOT_LOGGED_IN", exitCode: 3, message, hint });
|
|
24
|
+
}
|
|
25
|
+
};
|
|
26
|
+
var TokenExpiredError = class extends RatioCLIError {
|
|
27
|
+
constructor(message, hint = "Run 'ratio auth login' to refresh your session.") {
|
|
28
|
+
super({ code: "TOKEN_EXPIRED", exitCode: 3, message, hint });
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
var ValidationError = class extends RatioCLIError {
|
|
32
|
+
constructor(message, hint) {
|
|
33
|
+
const opts = {
|
|
34
|
+
code: "VALIDATION_ERROR",
|
|
35
|
+
exitCode: 8,
|
|
36
|
+
message
|
|
37
|
+
};
|
|
38
|
+
if (hint !== void 0) opts.hint = hint;
|
|
39
|
+
super(opts);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
// src/lib/credentials/clock.ts
|
|
44
|
+
var SystemClock = class {
|
|
45
|
+
now() {
|
|
46
|
+
return Date.now();
|
|
47
|
+
}
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
// src/lib/credentials/path.ts
|
|
51
|
+
import os from "os";
|
|
52
|
+
import path from "path";
|
|
53
|
+
function snapshotPathEnv() {
|
|
54
|
+
const snapshot = {};
|
|
55
|
+
const xdgConfigHome = process.env["XDG_CONFIG_HOME"];
|
|
56
|
+
if (xdgConfigHome !== void 0) {
|
|
57
|
+
snapshot.XDG_CONFIG_HOME = xdgConfigHome;
|
|
58
|
+
}
|
|
59
|
+
const appData = process.env["APPDATA"];
|
|
60
|
+
if (appData !== void 0) {
|
|
61
|
+
snapshot.APPDATA = appData;
|
|
62
|
+
}
|
|
63
|
+
return snapshot;
|
|
64
|
+
}
|
|
65
|
+
function defaultCredentialsPath(platform, homedir, env) {
|
|
66
|
+
const resolvedPlatform = platform ?? process.platform;
|
|
67
|
+
const resolvedHome = homedir ?? os.homedir();
|
|
68
|
+
const resolvedEnv = env ?? snapshotPathEnv();
|
|
69
|
+
if (resolvedPlatform === "win32") {
|
|
70
|
+
const appData = resolvedEnv.APPDATA;
|
|
71
|
+
const base2 = appData !== void 0 && appData !== "" ? appData : path.win32.join(resolvedHome, "AppData", "Roaming");
|
|
72
|
+
return path.win32.join(base2, "ratio", "credentials");
|
|
73
|
+
}
|
|
74
|
+
const xdgConfigHome = resolvedEnv.XDG_CONFIG_HOME;
|
|
75
|
+
const base = typeof xdgConfigHome === "string" && xdgConfigHome !== "" ? xdgConfigHome : path.posix.join(resolvedHome, ".config");
|
|
76
|
+
return path.posix.join(base, "ratio", "credentials");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// src/lib/credentials/refresher.ts
|
|
80
|
+
var NotImplementedRefresher = class {
|
|
81
|
+
async refresh(refreshToken) {
|
|
82
|
+
void refreshToken;
|
|
83
|
+
throw new NotLoggedInError(
|
|
84
|
+
"Token refresh is not available yet in this CLI version."
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
// src/lib/credentials/schema.ts
|
|
90
|
+
import { z } from "zod";
|
|
91
|
+
var identitySchema = z.object({
|
|
92
|
+
developerId: z.string(),
|
|
93
|
+
email: z.string(),
|
|
94
|
+
// Token-derived fields captured at login. Optional (additive) so files
|
|
95
|
+
// written before these fields existed still validate as version 1.
|
|
96
|
+
merchantId: z.string().optional(),
|
|
97
|
+
scopes: z.array(z.string()).optional()
|
|
98
|
+
}).strict();
|
|
99
|
+
var profileSchema = z.object({
|
|
100
|
+
accessToken: z.string(),
|
|
101
|
+
refreshToken: z.string(),
|
|
102
|
+
expiresAt: z.string().datetime({ offset: true }),
|
|
103
|
+
identity: identitySchema,
|
|
104
|
+
obtainedAt: z.string().datetime({ offset: true })
|
|
105
|
+
}).strict();
|
|
106
|
+
var developerSessionSchema = z.object({
|
|
107
|
+
jwt: z.string(),
|
|
108
|
+
developerId: z.string(),
|
|
109
|
+
email: z.string(),
|
|
110
|
+
expiresAt: z.string().datetime({ offset: true }),
|
|
111
|
+
obtainedAt: z.string().datetime({ offset: true })
|
|
112
|
+
}).strict();
|
|
113
|
+
var credentialsSchema = z.object({
|
|
114
|
+
version: z.literal(1),
|
|
115
|
+
server: z.string(),
|
|
116
|
+
clientId: z.string(),
|
|
117
|
+
profiles: z.record(z.string(), profileSchema),
|
|
118
|
+
// Optional cached developer session. Additive so files written before
|
|
119
|
+
// this field existed still validate as version 1.
|
|
120
|
+
developer: developerSessionSchema.optional()
|
|
121
|
+
}).strict();
|
|
122
|
+
function parseCredentials(input) {
|
|
123
|
+
if (typeof input === "object" && input !== null && "version" in input) {
|
|
124
|
+
const version = input.version;
|
|
125
|
+
if (version !== 1) {
|
|
126
|
+
const label = typeof version === "number" ? String(version) : "unknown";
|
|
127
|
+
throw new ValidationError(
|
|
128
|
+
`credentials file version ${label} is unsupported by this CLI \u2014 upgrade the CLI or sign in again`
|
|
129
|
+
);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
const result = credentialsSchema.safeParse(input);
|
|
133
|
+
if (!result.success) {
|
|
134
|
+
throw new ValidationError(
|
|
135
|
+
"credentials file schema mismatch \u2014 expected v1 shape"
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
return result.data;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// src/lib/credentials/store.ts
|
|
142
|
+
var REFRESH_WINDOW_MS = 3e5;
|
|
143
|
+
var ENV_TOKEN_VAR = "RATIO_API_TOKEN";
|
|
144
|
+
var ENV_DEVELOPER_TOKEN_VAR = "RATIO_DEVELOPER_TOKEN";
|
|
145
|
+
function hasErrnoCode(err, code) {
|
|
146
|
+
return err instanceof Error && err.code === code;
|
|
147
|
+
}
|
|
148
|
+
var FileCredentialStore = class {
|
|
149
|
+
filePath;
|
|
150
|
+
clock;
|
|
151
|
+
refresher;
|
|
152
|
+
envReader;
|
|
153
|
+
platform;
|
|
154
|
+
constructor(opts) {
|
|
155
|
+
this.filePath = opts?.filePath ?? defaultCredentialsPath();
|
|
156
|
+
this.clock = opts?.clock ?? new SystemClock();
|
|
157
|
+
this.refresher = opts?.refresher ?? new NotImplementedRefresher();
|
|
158
|
+
this.envReader = opts?.envReader ?? (() => process.env);
|
|
159
|
+
this.platform = opts?.platform ?? process.platform;
|
|
160
|
+
}
|
|
161
|
+
async load() {
|
|
162
|
+
this.reapplyFileModeIfDrifted();
|
|
163
|
+
let raw;
|
|
164
|
+
try {
|
|
165
|
+
raw = fs.readFileSync(this.filePath, "utf8");
|
|
166
|
+
} catch (err) {
|
|
167
|
+
if (hasErrnoCode(err, "ENOENT")) {
|
|
168
|
+
throw new NotLoggedInError("You are not signed in.");
|
|
169
|
+
}
|
|
170
|
+
throw err;
|
|
171
|
+
}
|
|
172
|
+
let parsed;
|
|
173
|
+
try {
|
|
174
|
+
parsed = JSON.parse(raw);
|
|
175
|
+
} catch {
|
|
176
|
+
throw new ValidationError(
|
|
177
|
+
'credentials file is not valid JSON \u2014 run "ratio auth login" to reset'
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
return parseCredentials(parsed);
|
|
181
|
+
}
|
|
182
|
+
async save(creds) {
|
|
183
|
+
const dir = dirname(this.filePath);
|
|
184
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
185
|
+
if (this.platform !== "win32") {
|
|
186
|
+
fs.chmodSync(dir, 448);
|
|
187
|
+
}
|
|
188
|
+
const tmpPath = `${this.filePath}.${process.pid}.${crypto.randomBytes(8).toString("hex")}.tmp`;
|
|
189
|
+
try {
|
|
190
|
+
fs.writeFileSync(tmpPath, JSON.stringify(creds, null, 2), {
|
|
191
|
+
mode: 384
|
|
192
|
+
});
|
|
193
|
+
fs.renameSync(tmpPath, this.filePath);
|
|
194
|
+
} catch (err) {
|
|
195
|
+
try {
|
|
196
|
+
fs.unlinkSync(tmpPath);
|
|
197
|
+
} catch {
|
|
198
|
+
}
|
|
199
|
+
throw err;
|
|
200
|
+
}
|
|
201
|
+
if (this.platform !== "win32") {
|
|
202
|
+
fs.chmodSync(this.filePath, 384);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
async clear() {
|
|
206
|
+
try {
|
|
207
|
+
fs.unlinkSync(this.filePath);
|
|
208
|
+
} catch (err) {
|
|
209
|
+
if (hasErrnoCode(err, "ENOENT")) {
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
throw err;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
async getValidAccessToken(profileName) {
|
|
216
|
+
const envToken = this.envReader()[ENV_TOKEN_VAR];
|
|
217
|
+
if (typeof envToken === "string" && envToken.length > 0) {
|
|
218
|
+
return envToken;
|
|
219
|
+
}
|
|
220
|
+
const creds = await this.load();
|
|
221
|
+
const name = profileName ?? "default";
|
|
222
|
+
const profile = creds.profiles[name];
|
|
223
|
+
if (profile === void 0) {
|
|
224
|
+
throw new NotLoggedInError("You are not signed in.");
|
|
225
|
+
}
|
|
226
|
+
const msUntilExpiry = new Date(profile.expiresAt).getTime() - this.clock.now();
|
|
227
|
+
if (msUntilExpiry < REFRESH_WINDOW_MS) {
|
|
228
|
+
return this.refreshAndPersist(creds, name, profile);
|
|
229
|
+
}
|
|
230
|
+
return profile.accessToken;
|
|
231
|
+
}
|
|
232
|
+
async getValidDeveloperToken() {
|
|
233
|
+
const envToken = this.envReader()[ENV_DEVELOPER_TOKEN_VAR];
|
|
234
|
+
if (typeof envToken === "string" && envToken.length > 0) {
|
|
235
|
+
return envToken;
|
|
236
|
+
}
|
|
237
|
+
let creds;
|
|
238
|
+
try {
|
|
239
|
+
creds = await this.load();
|
|
240
|
+
} catch (err) {
|
|
241
|
+
if (err instanceof NotLoggedInError) {
|
|
242
|
+
throw new NotLoggedInError("Developer signin required.");
|
|
243
|
+
}
|
|
244
|
+
throw err;
|
|
245
|
+
}
|
|
246
|
+
const session = creds.developer;
|
|
247
|
+
if (session === void 0) {
|
|
248
|
+
throw new NotLoggedInError("Developer signin required.");
|
|
249
|
+
}
|
|
250
|
+
const msUntilExpiry = new Date(session.expiresAt).getTime() - this.clock.now();
|
|
251
|
+
if (msUntilExpiry < REFRESH_WINDOW_MS) {
|
|
252
|
+
throw new NotLoggedInError("Developer signin required.");
|
|
253
|
+
}
|
|
254
|
+
return session.jwt;
|
|
255
|
+
}
|
|
256
|
+
async saveDeveloperSession(session) {
|
|
257
|
+
let creds;
|
|
258
|
+
try {
|
|
259
|
+
creds = await this.load();
|
|
260
|
+
} catch (err) {
|
|
261
|
+
if (err instanceof NotLoggedInError) {
|
|
262
|
+
creds = { version: 1, server: "", clientId: "", profiles: {} };
|
|
263
|
+
} else {
|
|
264
|
+
throw err;
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
const updated = {
|
|
268
|
+
...creds,
|
|
269
|
+
developer: session
|
|
270
|
+
};
|
|
271
|
+
await this.save(updated);
|
|
272
|
+
}
|
|
273
|
+
/**
|
|
274
|
+
* Exchange the profile's refresh token for a fresh token set, merge it
|
|
275
|
+
* into the loaded credentials (preserving every other profile), persist
|
|
276
|
+
* atomically, and return the new access token. A refresher failure is
|
|
277
|
+
* wrapped in TokenExpiredError; the underlying message is deliberately
|
|
278
|
+
* discarded so a secret can never leak through the error surface.
|
|
279
|
+
*/
|
|
280
|
+
async refreshAndPersist(creds, name, profile) {
|
|
281
|
+
let refreshed;
|
|
282
|
+
try {
|
|
283
|
+
refreshed = await this.refresher.refresh(profile.refreshToken);
|
|
284
|
+
} catch {
|
|
285
|
+
throw new TokenExpiredError(
|
|
286
|
+
"Your session has expired and could not be refreshed."
|
|
287
|
+
);
|
|
288
|
+
}
|
|
289
|
+
const updated = {
|
|
290
|
+
...creds,
|
|
291
|
+
profiles: {
|
|
292
|
+
...creds.profiles,
|
|
293
|
+
[name]: {
|
|
294
|
+
...profile,
|
|
295
|
+
accessToken: refreshed.accessToken,
|
|
296
|
+
refreshToken: refreshed.refreshToken,
|
|
297
|
+
expiresAt: refreshed.expiresAt
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
};
|
|
301
|
+
await this.save(updated);
|
|
302
|
+
return refreshed.accessToken;
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* POSIX-only startup permission check: if the file exists with a mode
|
|
306
|
+
* other than 0600, re-apply 0600 and emit a warning to stderr. Never
|
|
307
|
+
* runs on Windows (stat modes there do not map to POSIX bits and chmod
|
|
308
|
+
* must not be called). Missing file is NOT an error here — load()
|
|
309
|
+
* reports it as NotLoggedInError.
|
|
310
|
+
*/
|
|
311
|
+
reapplyFileModeIfDrifted() {
|
|
312
|
+
if (this.platform === "win32") {
|
|
313
|
+
return;
|
|
314
|
+
}
|
|
315
|
+
let stat;
|
|
316
|
+
try {
|
|
317
|
+
stat = fs.statSync(this.filePath);
|
|
318
|
+
} catch (err) {
|
|
319
|
+
if (hasErrnoCode(err, "ENOENT")) {
|
|
320
|
+
return;
|
|
321
|
+
}
|
|
322
|
+
throw err;
|
|
323
|
+
}
|
|
324
|
+
const mode = stat.mode & 511;
|
|
325
|
+
if (mode !== 384) {
|
|
326
|
+
fs.chmodSync(this.filePath, 384);
|
|
327
|
+
process.stderr.write(
|
|
328
|
+
`[warn] credentials file permissions were 0o${mode.toString(8)}; corrected to 0600.
|
|
329
|
+
`
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
};
|
|
334
|
+
export {
|
|
335
|
+
FileCredentialStore
|
|
336
|
+
};
|
|
337
|
+
//# sourceMappingURL=store.js.map
|