@getpaseo/cli 0.3.0 → 0.3.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/dist/commands/hub/authority.d.ts +17 -0
- package/dist/commands/hub/authority.js +18 -0
- package/dist/commands/hub/client.d.ts +55 -3
- package/dist/commands/hub/client.js +158 -38
- package/dist/commands/hub/connect.d.ts +21 -0
- package/dist/commands/hub/connect.js +34 -0
- package/dist/commands/hub/credentials.d.ts +21 -0
- package/dist/commands/hub/credentials.js +143 -0
- package/dist/commands/hub/daemon-client.d.ts +27 -0
- package/dist/commands/hub/daemon-client.js +14 -0
- package/dist/commands/hub/deploy-input.js +28 -28
- package/dist/commands/hub/deploy.d.ts +23 -3
- package/dist/commands/hub/deploy.js +51 -42
- package/dist/commands/hub/disconnect.d.ts +16 -0
- package/dist/commands/hub/disconnect.js +24 -0
- package/dist/commands/hub/error.d.ts +1 -1
- package/dist/commands/hub/error.js +2 -2
- package/dist/commands/hub/help.d.ts +3 -0
- package/dist/commands/hub/help.js +5 -0
- package/dist/commands/hub/index.d.ts +15 -25
- package/dist/commands/hub/index.js +64 -74
- package/dist/commands/hub/{device-authorization.d.ts → login-flow.d.ts} +12 -15
- package/dist/commands/hub/login-flow.js +78 -0
- package/dist/commands/hub/login.d.ts +21 -0
- package/dist/commands/hub/login.js +34 -0
- package/dist/commands/hub/logout.d.ts +31 -0
- package/dist/commands/hub/logout.js +65 -0
- package/dist/commands/hub/origin.d.ts +2 -0
- package/dist/commands/hub/origin.js +27 -0
- package/dist/commands/hub/projects.d.ts +24 -0
- package/dist/commands/hub/projects.js +49 -0
- package/dist/commands/hub/reporter.d.ts +10 -0
- package/dist/commands/hub/reporter.js +11 -0
- package/dist/commands/hub/status-output.d.ts +13 -0
- package/dist/commands/hub/status-output.js +30 -0
- package/dist/commands/schedule/types.d.ts +1 -4
- package/dist/output/types.d.ts +1 -1
- package/package.json +4 -4
- package/dist/commands/hub/cloud-device-authorization.d.ts +0 -45
- package/dist/commands/hub/cloud-device-authorization.js +0 -92
- package/dist/commands/hub/device-authorization.js +0 -87
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { HubCredentialStore } from "./credentials.js";
|
|
2
|
+
export interface HubAuthorityOptions {
|
|
3
|
+
origin?: string;
|
|
4
|
+
apiKey?: string;
|
|
5
|
+
}
|
|
6
|
+
interface ResolveHubInput {
|
|
7
|
+
options: HubAuthorityOptions;
|
|
8
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
9
|
+
credentials: HubCredentialStore;
|
|
10
|
+
}
|
|
11
|
+
export declare const DEFAULT_HUB_ORIGIN = "https://hub.paseo.sh";
|
|
12
|
+
export declare function resolveHubOrigin(input: ResolveHubInput): string;
|
|
13
|
+
export declare function resolveHubCredential(input: ResolveHubInput & {
|
|
14
|
+
origin: string;
|
|
15
|
+
}): string;
|
|
16
|
+
export {};
|
|
17
|
+
//# sourceMappingURL=authority.d.ts.map
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { HubCommandError } from "./error.js";
|
|
2
|
+
import { normalizeHubOrigin } from "./origin.js";
|
|
3
|
+
export const DEFAULT_HUB_ORIGIN = "https://hub.paseo.sh";
|
|
4
|
+
export function resolveHubOrigin(input) {
|
|
5
|
+
const configuredOrigin = input.options.origin ?? input.env.PASEO_HUB_URL;
|
|
6
|
+
const selectedOrigin = configuredOrigin ?? input.credentials.active()?.origin ?? DEFAULT_HUB_ORIGIN;
|
|
7
|
+
return normalizeHubOrigin(selectedOrigin);
|
|
8
|
+
}
|
|
9
|
+
export function resolveHubCredential(input) {
|
|
10
|
+
const explicitCredential = input.options.apiKey ?? input.env.PASEO_HUB_API_KEY;
|
|
11
|
+
if (explicitCredential !== undefined)
|
|
12
|
+
return explicitCredential;
|
|
13
|
+
const stored = input.credentials.get(input.origin);
|
|
14
|
+
if (stored !== null)
|
|
15
|
+
return stored.credential;
|
|
16
|
+
throw new HubCommandError("HUB_API_KEY_REQUIRED", `No stored Hub login matches ${input.origin}. Run \`paseo hub login ${input.origin}\`, pass --api-key <secret>, or set PASEO_HUB_API_KEY.`);
|
|
17
|
+
}
|
|
18
|
+
//# sourceMappingURL=authority.js.map
|
|
@@ -1,19 +1,71 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import type { HubDeployPartial } from "./deploy-input.js";
|
|
3
|
+
declare const authorizationSchema: z.ZodObject<{
|
|
4
|
+
deviceCode: z.ZodString;
|
|
5
|
+
userCode: z.ZodString;
|
|
6
|
+
verificationUri: z.ZodURL;
|
|
7
|
+
verificationUriComplete: z.ZodURL;
|
|
8
|
+
expiresAt: z.ZodString;
|
|
9
|
+
interval: z.ZodNumber;
|
|
10
|
+
}, z.core.$strict>;
|
|
11
|
+
declare const authorizationPollSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
|
|
12
|
+
status: z.ZodLiteral<"pending">;
|
|
13
|
+
interval: z.ZodNumber;
|
|
14
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
15
|
+
status: z.ZodLiteral<"slow_down">;
|
|
16
|
+
interval: z.ZodNumber;
|
|
17
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
18
|
+
status: z.ZodLiteral<"authorized">;
|
|
19
|
+
interval: z.ZodNumber;
|
|
20
|
+
credential: z.ZodString;
|
|
21
|
+
organizationId: z.ZodString;
|
|
22
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
23
|
+
status: z.ZodLiteral<"denied">;
|
|
24
|
+
interval: z.ZodNumber;
|
|
25
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
26
|
+
status: z.ZodLiteral<"expired">;
|
|
27
|
+
interval: z.ZodNumber;
|
|
28
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
29
|
+
status: z.ZodLiteral<"disclosed">;
|
|
30
|
+
interval: z.ZodNumber;
|
|
31
|
+
}, z.core.$strict>, z.ZodObject<{
|
|
32
|
+
status: z.ZodLiteral<"retry_later">;
|
|
33
|
+
}, z.core.$strict>], "status">;
|
|
34
|
+
declare const projectSchema: z.ZodObject<{
|
|
35
|
+
id: z.ZodString;
|
|
36
|
+
slug: z.ZodString;
|
|
37
|
+
name: z.ZodString;
|
|
38
|
+
}, z.core.$strict>;
|
|
3
39
|
declare const installResponseSchema: z.ZodObject<{
|
|
4
40
|
projectSlug: z.ZodString;
|
|
5
41
|
version: z.ZodNumber;
|
|
6
42
|
versionId: z.ZodString;
|
|
7
|
-
active: z.
|
|
43
|
+
active: z.ZodLiteral<true>;
|
|
8
44
|
}, z.core.$strict>;
|
|
45
|
+
declare const validationResponseSchema: z.ZodObject<{
|
|
46
|
+
projectSlug: z.ZodString;
|
|
47
|
+
valid: z.ZodLiteral<true>;
|
|
48
|
+
}, z.core.$strict>;
|
|
49
|
+
export type CliAuthorization = z.infer<typeof authorizationSchema>;
|
|
50
|
+
export type CliAuthorizationPoll = z.infer<typeof authorizationPollSchema>;
|
|
51
|
+
export type HubProject = z.infer<typeof projectSchema>;
|
|
9
52
|
export type HubInstallResult = z.infer<typeof installResponseSchema>;
|
|
10
|
-
|
|
53
|
+
export type HubValidationResult = z.infer<typeof validationResponseSchema>;
|
|
54
|
+
interface HubConfigurationInput {
|
|
11
55
|
origin: string;
|
|
12
56
|
apiKey: string;
|
|
13
57
|
projectSlug: string;
|
|
14
58
|
yaml: string;
|
|
15
59
|
partials?: readonly HubDeployPartial[];
|
|
16
60
|
}
|
|
17
|
-
export declare
|
|
61
|
+
export declare class HubHttpClient {
|
|
62
|
+
startCliAuthorization(origin: string): Promise<CliAuthorization>;
|
|
63
|
+
pollCliAuthorization(origin: string, deviceCode: string, timeoutMilliseconds: number): Promise<CliAuthorizationPoll>;
|
|
64
|
+
listProjects(origin: string, apiKey: string): Promise<HubProject[]>;
|
|
65
|
+
installConfiguration(input: HubConfigurationInput): Promise<HubInstallResult>;
|
|
66
|
+
validateConfiguration(input: HubConfigurationInput): Promise<HubValidationResult>;
|
|
67
|
+
issueEnrollmentToken(origin: string, apiKey: string): Promise<string>;
|
|
68
|
+
private request;
|
|
69
|
+
}
|
|
18
70
|
export {};
|
|
19
71
|
//# sourceMappingURL=client.d.ts.map
|
|
@@ -1,13 +1,50 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
-
import {
|
|
2
|
+
import { HubCommandError } from "./error.js";
|
|
3
|
+
const activationUrlSchema = z.url({ protocol: /^https?$/u });
|
|
4
|
+
const authorizationSchema = z
|
|
5
|
+
.object({
|
|
6
|
+
deviceCode: z.string().min(32),
|
|
7
|
+
userCode: z.string().min(1),
|
|
8
|
+
verificationUri: activationUrlSchema,
|
|
9
|
+
verificationUriComplete: activationUrlSchema,
|
|
10
|
+
expiresAt: z.string().datetime(),
|
|
11
|
+
interval: z.number().int().min(1),
|
|
12
|
+
})
|
|
13
|
+
.strict();
|
|
14
|
+
const authorizationPollSchema = z.discriminatedUnion("status", [
|
|
15
|
+
z.object({ status: z.literal("pending"), interval: z.number().int().min(1) }).strict(),
|
|
16
|
+
z.object({ status: z.literal("slow_down"), interval: z.number().int().min(1) }).strict(),
|
|
17
|
+
z
|
|
18
|
+
.object({
|
|
19
|
+
status: z.literal("authorized"),
|
|
20
|
+
interval: z.number().int().min(1),
|
|
21
|
+
credential: z.string().min(32),
|
|
22
|
+
organizationId: z.string().min(1),
|
|
23
|
+
})
|
|
24
|
+
.strict(),
|
|
25
|
+
z.object({ status: z.literal("denied"), interval: z.number().int().min(1) }).strict(),
|
|
26
|
+
z.object({ status: z.literal("expired"), interval: z.number().int().min(1) }).strict(),
|
|
27
|
+
z.object({ status: z.literal("disclosed"), interval: z.number().int().min(1) }).strict(),
|
|
28
|
+
z.object({ status: z.literal("retry_later") }).strict(),
|
|
29
|
+
]);
|
|
30
|
+
const projectSchema = z
|
|
31
|
+
.object({ id: z.string().uuid(), slug: z.string().min(1), name: z.string().min(1) })
|
|
32
|
+
.strict();
|
|
33
|
+
const projectsResponseSchema = z.object({ projects: z.array(projectSchema) }).strict();
|
|
3
34
|
const installResponseSchema = z
|
|
4
35
|
.object({
|
|
5
36
|
projectSlug: z.string().min(1),
|
|
6
37
|
version: z.number().int().positive(),
|
|
7
38
|
versionId: z.string().uuid(),
|
|
8
|
-
active: z.
|
|
39
|
+
active: z.literal(true),
|
|
9
40
|
})
|
|
10
41
|
.strict();
|
|
42
|
+
const validationResponseSchema = z
|
|
43
|
+
.object({ projectSlug: z.string().min(1), valid: z.literal(true) })
|
|
44
|
+
.strict();
|
|
45
|
+
const enrollmentTokenSchema = z
|
|
46
|
+
.object({ token: z.string().min(32), expiresAt: z.string().datetime() })
|
|
47
|
+
.strict();
|
|
11
48
|
const issuePathSchema = z.union([z.string(), z.array(z.union([z.string(), z.number()]))]);
|
|
12
49
|
const fieldIssueSchema = z.object({
|
|
13
50
|
field: z.string().optional(),
|
|
@@ -25,60 +62,145 @@ const problemSchema = z.object({
|
|
|
25
62
|
.optional(),
|
|
26
63
|
issues: z.array(fieldIssueSchema).optional(),
|
|
27
64
|
});
|
|
28
|
-
export
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
65
|
+
export class HubHttpClient {
|
|
66
|
+
async startCliAuthorization(origin) {
|
|
67
|
+
return this.request({
|
|
68
|
+
origin,
|
|
69
|
+
path: "/api/v1/cli-authorizations",
|
|
32
70
|
method: "POST",
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
body: JSON.stringify({
|
|
38
|
-
projectSlug: input.projectSlug,
|
|
39
|
-
yaml: input.yaml,
|
|
40
|
-
...(input.partials === undefined || input.partials.length === 0
|
|
41
|
-
? {}
|
|
42
|
-
: { partials: input.partials }),
|
|
43
|
-
}),
|
|
71
|
+
body: {},
|
|
72
|
+
successStatus: 201,
|
|
73
|
+
schema: authorizationSchema,
|
|
74
|
+
failureMessage: "Hub CLI login could not be started",
|
|
44
75
|
});
|
|
45
76
|
}
|
|
46
|
-
|
|
47
|
-
|
|
77
|
+
async pollCliAuthorization(origin, deviceCode, timeoutMilliseconds) {
|
|
78
|
+
try {
|
|
79
|
+
return await this.request({
|
|
80
|
+
origin,
|
|
81
|
+
path: "/api/v1/cli-authorizations/poll",
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: { deviceCode },
|
|
84
|
+
successStatus: 200,
|
|
85
|
+
schema: authorizationPollSchema,
|
|
86
|
+
timeoutMilliseconds,
|
|
87
|
+
failureMessage: "Hub CLI login polling failed",
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
catch (error) {
|
|
91
|
+
if (error instanceof HubCommandError && error.code === "HUB_NETWORK_ERROR") {
|
|
92
|
+
return { status: "retry_later" };
|
|
93
|
+
}
|
|
94
|
+
throw error;
|
|
95
|
+
}
|
|
48
96
|
}
|
|
49
|
-
|
|
50
|
-
|
|
97
|
+
async listProjects(origin, apiKey) {
|
|
98
|
+
const response = await this.request({
|
|
99
|
+
origin,
|
|
100
|
+
path: "/api/v1/projects",
|
|
101
|
+
method: "GET",
|
|
102
|
+
apiKey,
|
|
103
|
+
successStatus: 200,
|
|
104
|
+
schema: projectsResponseSchema,
|
|
105
|
+
failureMessage: "Hub project listing failed",
|
|
106
|
+
});
|
|
107
|
+
return response.projects;
|
|
51
108
|
}
|
|
52
|
-
|
|
53
|
-
return
|
|
109
|
+
installConfiguration(input) {
|
|
110
|
+
return this.request({
|
|
111
|
+
origin: input.origin,
|
|
112
|
+
path: "/api/v1/configurations/install",
|
|
113
|
+
method: "POST",
|
|
114
|
+
apiKey: input.apiKey,
|
|
115
|
+
body: configurationBody(input),
|
|
116
|
+
successStatus: 201,
|
|
117
|
+
schema: installResponseSchema,
|
|
118
|
+
failureMessage: "Hub deployment failed",
|
|
119
|
+
});
|
|
54
120
|
}
|
|
55
|
-
|
|
56
|
-
|
|
121
|
+
validateConfiguration(input) {
|
|
122
|
+
return this.request({
|
|
123
|
+
origin: input.origin,
|
|
124
|
+
path: "/api/v1/configurations/validate",
|
|
125
|
+
method: "POST",
|
|
126
|
+
apiKey: input.apiKey,
|
|
127
|
+
body: configurationBody(input),
|
|
128
|
+
successStatus: 200,
|
|
129
|
+
schema: validationResponseSchema,
|
|
130
|
+
failureMessage: "Hub configuration validation failed",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
async issueEnrollmentToken(origin, apiKey) {
|
|
134
|
+
const response = await this.request({
|
|
135
|
+
origin,
|
|
136
|
+
path: "/api/v1/daemons/enrollment-tokens",
|
|
137
|
+
method: "POST",
|
|
138
|
+
apiKey,
|
|
139
|
+
successStatus: 201,
|
|
140
|
+
schema: enrollmentTokenSchema,
|
|
141
|
+
failureMessage: "Hub daemon enrollment authorization failed",
|
|
142
|
+
});
|
|
143
|
+
return response.token;
|
|
144
|
+
}
|
|
145
|
+
async request(input) {
|
|
146
|
+
const signal = AbortSignal.timeout(input.timeoutMilliseconds ?? 15000);
|
|
147
|
+
let response;
|
|
148
|
+
try {
|
|
149
|
+
response = await fetch(`${input.origin}${input.path}`, {
|
|
150
|
+
method: input.method,
|
|
151
|
+
headers: {
|
|
152
|
+
...(input.apiKey === undefined ? {} : { authorization: `Bearer ${input.apiKey}` }),
|
|
153
|
+
...(input.body === undefined ? {} : { "content-type": "application/json" }),
|
|
154
|
+
},
|
|
155
|
+
...(input.body === undefined ? {} : { body: JSON.stringify(input.body) }),
|
|
156
|
+
signal,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
catch {
|
|
160
|
+
throw new HubCommandError("HUB_NETWORK_ERROR", `Could not reach Paseo Hub at ${input.origin}. Check the Hub URL and network connection.`);
|
|
161
|
+
}
|
|
162
|
+
if (response.status !== input.successStatus) {
|
|
163
|
+
throw await requestFailure(response, input.failureMessage, input.apiKey);
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
return input.schema.parse(await response.json());
|
|
167
|
+
}
|
|
168
|
+
catch {
|
|
169
|
+
throw new HubCommandError("HUB_INVALID_RESPONSE", "Hub returned a malformed response.");
|
|
170
|
+
}
|
|
57
171
|
}
|
|
58
172
|
}
|
|
59
|
-
|
|
173
|
+
function configurationBody(input) {
|
|
174
|
+
return {
|
|
175
|
+
projectSlug: input.projectSlug,
|
|
176
|
+
yaml: input.yaml,
|
|
177
|
+
...(input.partials === undefined || input.partials.length === 0
|
|
178
|
+
? {}
|
|
179
|
+
: { partials: input.partials }),
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
async function requestFailure(response, failureMessage, apiKey) {
|
|
60
183
|
const contentType = response.headers.get("content-type") ?? "";
|
|
61
184
|
if (!contentType.toLowerCase().includes("application/problem+json")) {
|
|
62
|
-
return new
|
|
185
|
+
return new HubCommandError("HUB_REQUEST_FAILED", `${failureMessage} with HTTP ${response.status}.`);
|
|
63
186
|
}
|
|
64
187
|
let body;
|
|
65
188
|
try {
|
|
66
189
|
body = await response.json();
|
|
67
190
|
}
|
|
68
191
|
catch {
|
|
69
|
-
return new
|
|
192
|
+
return new HubCommandError("HUB_INVALID_RESPONSE", `Hub returned malformed problem details for HTTP ${response.status}.`);
|
|
70
193
|
}
|
|
71
194
|
const parsed = problemSchema.safeParse(body);
|
|
72
195
|
if (!parsed.success ||
|
|
73
196
|
(parsed.data.status !== undefined && parsed.data.status !== response.status)) {
|
|
74
|
-
return new
|
|
197
|
+
return new HubCommandError("HUB_INVALID_RESPONSE", `Hub returned nonconforming problem details for HTTP ${response.status}.`);
|
|
75
198
|
}
|
|
76
|
-
const title = parsed.data.title ??
|
|
77
|
-
const
|
|
78
|
-
const message = detail === undefined ? title : `${title}: ${detail}`;
|
|
199
|
+
const title = parsed.data.title ?? `${failureMessage} with HTTP ${response.status}`;
|
|
200
|
+
const message = parsed.data.detail === undefined ? title : `${title}: ${parsed.data.detail}`;
|
|
79
201
|
const details = formatFieldIssues(parsed.data.errors, parsed.data.issues);
|
|
80
202
|
const code = response.status === 422 ? "HUB_VALIDATION_FAILED" : "HUB_REQUEST_FAILED";
|
|
81
|
-
return new
|
|
203
|
+
return new HubCommandError(code, redactSecret(message, apiKey), details === undefined ? undefined : redactSecret(details, apiKey));
|
|
82
204
|
}
|
|
83
205
|
function formatFieldIssues(errors, issues) {
|
|
84
206
|
const fieldIssues = Array.isArray(errors) ? errors : issues;
|
|
@@ -99,16 +221,14 @@ function formatIssuePath(path) {
|
|
|
99
221
|
return path;
|
|
100
222
|
let formatted = "";
|
|
101
223
|
for (const segment of path) {
|
|
102
|
-
if (typeof segment === "number")
|
|
224
|
+
if (typeof segment === "number")
|
|
103
225
|
formatted += `[${segment}]`;
|
|
104
|
-
|
|
105
|
-
else {
|
|
226
|
+
else
|
|
106
227
|
formatted += formatted.length === 0 ? segment : `.${segment}`;
|
|
107
|
-
}
|
|
108
228
|
}
|
|
109
229
|
return formatted || undefined;
|
|
110
230
|
}
|
|
111
231
|
function redactSecret(value, secret) {
|
|
112
|
-
return value.split(secret).join("[redacted]");
|
|
232
|
+
return secret === undefined ? value : value.split(secret).join("[redacted]");
|
|
113
233
|
}
|
|
114
234
|
//# sourceMappingURL=client.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { Command } from "commander";
|
|
2
|
+
import type { HubHttpClient } from "./client.js";
|
|
3
|
+
import type { HubCredentialStore } from "./credentials.js";
|
|
4
|
+
import type { HubDaemonConnection } from "./daemon-client.js";
|
|
5
|
+
import { type HubReporter } from "./reporter.js";
|
|
6
|
+
interface HubConnectOptions {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
host?: string;
|
|
9
|
+
json?: boolean;
|
|
10
|
+
}
|
|
11
|
+
interface HubConnectDependencies {
|
|
12
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
13
|
+
credentials: HubCredentialStore;
|
|
14
|
+
hub: Pick<HubHttpClient, "issueEnrollmentToken">;
|
|
15
|
+
daemon: HubDaemonConnection;
|
|
16
|
+
reporter: HubReporter;
|
|
17
|
+
}
|
|
18
|
+
export declare function runHubConnect(originInput: string | undefined, options: HubConnectOptions, dependencies: HubConnectDependencies): Promise<import("../../output/types.js").ListResult<import("./status-output.js").HubRow>>;
|
|
19
|
+
export declare function addHubConnectCommand(parent: Command, dependencies: HubConnectDependencies): void;
|
|
20
|
+
export {};
|
|
21
|
+
//# sourceMappingURL=connect.d.ts.map
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { withOutput } from "../../output/index.js";
|
|
2
|
+
import { addJsonAndDaemonHostOptions } from "../../utils/command-options.js";
|
|
3
|
+
import { resolveHubCredential, resolveHubOrigin } from "./authority.js";
|
|
4
|
+
import { withHubDaemon } from "./daemon-client.js";
|
|
5
|
+
import { hubStatusResult } from "./status-output.js";
|
|
6
|
+
import { reportHubProgress } from "./reporter.js";
|
|
7
|
+
import { addHubResolutionHelp } from "./help.js";
|
|
8
|
+
export async function runHubConnect(originInput, options, dependencies) {
|
|
9
|
+
const resolution = {
|
|
10
|
+
options: { origin: originInput, apiKey: options.apiKey },
|
|
11
|
+
env: dependencies.env,
|
|
12
|
+
credentials: dependencies.credentials,
|
|
13
|
+
};
|
|
14
|
+
const origin = resolveHubOrigin(resolution);
|
|
15
|
+
reportHubProgress(dependencies.reporter, options, `Connecting this daemon to ${origin}`);
|
|
16
|
+
const credential = resolveHubCredential({ ...resolution, origin });
|
|
17
|
+
const token = await dependencies.hub.issueEnrollmentToken(origin, credential);
|
|
18
|
+
return withHubDaemon(dependencies.daemon, options.host, async (daemon) => {
|
|
19
|
+
const response = await daemon.connectHub(origin, token);
|
|
20
|
+
return hubStatusResult(response.status);
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function addHubConnectCommand(parent, dependencies) {
|
|
24
|
+
addJsonAndDaemonHostOptions(addHubResolutionHelp(parent
|
|
25
|
+
.command("connect")
|
|
26
|
+
.description("Enroll this daemon with a Paseo Hub")
|
|
27
|
+
.argument("[origin]", "Paseo Hub origin")
|
|
28
|
+
.option("--api-key <secret>", "Organization API key"))).action(withOutput(async (...args) => {
|
|
29
|
+
const origin = args[0];
|
|
30
|
+
const options = args.at(-2);
|
|
31
|
+
return runHubConnect(origin, options, dependencies);
|
|
32
|
+
}));
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=connect.js.map
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
export interface StoredHubCredential {
|
|
2
|
+
origin: string;
|
|
3
|
+
credential: string;
|
|
4
|
+
}
|
|
5
|
+
export interface HubCredentialStore {
|
|
6
|
+
active(): StoredHubCredential | null;
|
|
7
|
+
get(origin: string): StoredHubCredential | null;
|
|
8
|
+
save(credential: StoredHubCredential): void;
|
|
9
|
+
logoutActive(): StoredHubCredential | null;
|
|
10
|
+
}
|
|
11
|
+
export declare class PrivateHubCredentialStore implements HubCredentialStore {
|
|
12
|
+
private readonly filePath;
|
|
13
|
+
constructor(env?: Readonly<Record<string, string | undefined>>);
|
|
14
|
+
active(): StoredHubCredential | null;
|
|
15
|
+
get(origin: string): StoredHubCredential | null;
|
|
16
|
+
save(credential: StoredHubCredential): void;
|
|
17
|
+
logoutActive(): StoredHubCredential | null;
|
|
18
|
+
private read;
|
|
19
|
+
private write;
|
|
20
|
+
}
|
|
21
|
+
//# sourceMappingURL=credentials.d.ts.map
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { z } from "zod";
|
|
6
|
+
import { HubCommandError } from "./error.js";
|
|
7
|
+
import { normalizeHubOrigin } from "./origin.js";
|
|
8
|
+
const PRIVATE_DIRECTORY_MODE = 0o700;
|
|
9
|
+
const PRIVATE_FILE_MODE = 0o600;
|
|
10
|
+
const credentialRecordSchema = z
|
|
11
|
+
.object({ origin: z.string(), credential: z.string().min(1) })
|
|
12
|
+
.strict();
|
|
13
|
+
const credentialFileSchema = z
|
|
14
|
+
.object({
|
|
15
|
+
version: z.literal(1),
|
|
16
|
+
activeOrigin: z.string().nullable(),
|
|
17
|
+
credentials: z.array(credentialRecordSchema),
|
|
18
|
+
})
|
|
19
|
+
.strict()
|
|
20
|
+
.superRefine((value, context) => {
|
|
21
|
+
const origins = new Set();
|
|
22
|
+
for (const record of value.credentials) {
|
|
23
|
+
let normalized;
|
|
24
|
+
try {
|
|
25
|
+
normalized = normalizeHubOrigin(record.origin);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
context.addIssue({ code: "custom", message: "Credential origin is invalid" });
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (normalized !== record.origin || origins.has(record.origin)) {
|
|
32
|
+
context.addIssue({
|
|
33
|
+
code: "custom",
|
|
34
|
+
message: "Credential origins must be unique and normalized",
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
origins.add(record.origin);
|
|
38
|
+
}
|
|
39
|
+
if (value.activeOrigin !== null && !origins.has(value.activeOrigin)) {
|
|
40
|
+
context.addIssue({ code: "custom", message: "Active credential is missing" });
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
export class PrivateHubCredentialStore {
|
|
44
|
+
constructor(env = process.env) {
|
|
45
|
+
this.filePath = path.join(resolvePaseoHome(env), "hub-credentials.json");
|
|
46
|
+
}
|
|
47
|
+
active() {
|
|
48
|
+
const data = this.read();
|
|
49
|
+
if (data.activeOrigin === null)
|
|
50
|
+
return null;
|
|
51
|
+
return data.credentials.find((record) => record.origin === data.activeOrigin) ?? null;
|
|
52
|
+
}
|
|
53
|
+
get(origin) {
|
|
54
|
+
const normalizedOrigin = normalizeHubOrigin(origin);
|
|
55
|
+
return this.read().credentials.find((record) => record.origin === normalizedOrigin) ?? null;
|
|
56
|
+
}
|
|
57
|
+
save(credential) {
|
|
58
|
+
const origin = normalizeHubOrigin(credential.origin);
|
|
59
|
+
const current = this.read();
|
|
60
|
+
const remaining = current.credentials.filter((record) => record.origin !== origin);
|
|
61
|
+
this.write({
|
|
62
|
+
version: 1,
|
|
63
|
+
activeOrigin: origin,
|
|
64
|
+
credentials: [...remaining, { origin, credential: credential.credential }],
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
logoutActive() {
|
|
68
|
+
const current = this.read();
|
|
69
|
+
if (current.activeOrigin === null)
|
|
70
|
+
return null;
|
|
71
|
+
const removed = current.credentials.find((record) => record.origin === current.activeOrigin);
|
|
72
|
+
if (removed === undefined)
|
|
73
|
+
throw invalidCredentialFile();
|
|
74
|
+
this.write({
|
|
75
|
+
version: 1,
|
|
76
|
+
activeOrigin: null,
|
|
77
|
+
credentials: current.credentials.filter((record) => record.origin !== current.activeOrigin),
|
|
78
|
+
});
|
|
79
|
+
return removed;
|
|
80
|
+
}
|
|
81
|
+
read() {
|
|
82
|
+
let contents;
|
|
83
|
+
try {
|
|
84
|
+
contents = readFileSync(this.filePath, "utf8");
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (isMissingFile(error))
|
|
88
|
+
return { version: 1, activeOrigin: null, credentials: [] };
|
|
89
|
+
throw credentialStorageError();
|
|
90
|
+
}
|
|
91
|
+
chmodPrivate(this.filePath, PRIVATE_FILE_MODE);
|
|
92
|
+
try {
|
|
93
|
+
return credentialFileSchema.parse(JSON.parse(contents));
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
throw invalidCredentialFile();
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
write(value) {
|
|
100
|
+
const parsed = credentialFileSchema.parse(value);
|
|
101
|
+
const directory = path.dirname(this.filePath);
|
|
102
|
+
mkdirSync(directory, { recursive: true, mode: PRIVATE_DIRECTORY_MODE });
|
|
103
|
+
chmodPrivate(directory, PRIVATE_DIRECTORY_MODE);
|
|
104
|
+
const temporaryPath = path.join(directory, `.${path.basename(this.filePath)}.${process.pid}.${randomUUID()}.tmp`);
|
|
105
|
+
try {
|
|
106
|
+
writeFileSync(temporaryPath, `${JSON.stringify(parsed, null, 2)}\n`, {
|
|
107
|
+
encoding: "utf8",
|
|
108
|
+
mode: PRIVATE_FILE_MODE,
|
|
109
|
+
});
|
|
110
|
+
renameSync(temporaryPath, this.filePath);
|
|
111
|
+
chmodPrivate(this.filePath, PRIVATE_FILE_MODE);
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
rmSync(temporaryPath, { force: true });
|
|
115
|
+
throw credentialStorageError();
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
function resolvePaseoHome(env) {
|
|
120
|
+
const configured = env.PASEO_HOME ?? "~/.paseo";
|
|
121
|
+
const expanded = configured === "~" ? homedir() : configured.replace(/^~\//u, `${homedir()}/`);
|
|
122
|
+
return path.resolve(expanded);
|
|
123
|
+
}
|
|
124
|
+
function chmodPrivate(target, mode) {
|
|
125
|
+
if (process.platform === "win32")
|
|
126
|
+
return;
|
|
127
|
+
try {
|
|
128
|
+
chmodSync(target, mode);
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
throw credentialStorageError();
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
function isMissingFile(error) {
|
|
135
|
+
return error instanceof Error && "code" in error && error.code === "ENOENT";
|
|
136
|
+
}
|
|
137
|
+
function invalidCredentialFile() {
|
|
138
|
+
return new HubCommandError("HUB_CREDENTIALS_INVALID", "Stored Hub login is invalid. Run `paseo hub login <origin>` to replace it.");
|
|
139
|
+
}
|
|
140
|
+
function credentialStorageError() {
|
|
141
|
+
return new HubCommandError("HUB_CREDENTIALS_UNAVAILABLE", "Could not access the private Hub credential store under PASEO_HOME.");
|
|
142
|
+
}
|
|
143
|
+
//# sourceMappingURL=credentials.js.map
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
export interface HubStatus {
|
|
2
|
+
state: string;
|
|
3
|
+
daemonId: string | null;
|
|
4
|
+
hubOrigin: string | null;
|
|
5
|
+
scopes: string[];
|
|
6
|
+
connectedAt: string | null;
|
|
7
|
+
lastError: string | null;
|
|
8
|
+
}
|
|
9
|
+
export interface HubDaemonClient {
|
|
10
|
+
connectHub(url: string, token: string): Promise<{
|
|
11
|
+
status: HubStatus;
|
|
12
|
+
}>;
|
|
13
|
+
getHubStatus(): Promise<{
|
|
14
|
+
status: HubStatus;
|
|
15
|
+
}>;
|
|
16
|
+
disconnectHub(force: boolean): Promise<{
|
|
17
|
+
status: HubStatus;
|
|
18
|
+
warning?: string;
|
|
19
|
+
}>;
|
|
20
|
+
close(): Promise<void>;
|
|
21
|
+
}
|
|
22
|
+
export interface HubDaemonConnection {
|
|
23
|
+
connect(host: string | undefined): Promise<HubDaemonClient>;
|
|
24
|
+
}
|
|
25
|
+
export declare const productionHubDaemonConnection: HubDaemonConnection;
|
|
26
|
+
export declare function withHubDaemon<T>(connection: HubDaemonConnection, host: string | undefined, action: (client: HubDaemonClient) => Promise<T>): Promise<T>;
|
|
27
|
+
//# sourceMappingURL=daemon-client.d.ts.map
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { connectToDaemon } from "../../utils/client.js";
|
|
2
|
+
export const productionHubDaemonConnection = {
|
|
3
|
+
connect: (host) => connectToDaemon({ host }),
|
|
4
|
+
};
|
|
5
|
+
export async function withHubDaemon(connection, host, action) {
|
|
6
|
+
const client = await connection.connect(host);
|
|
7
|
+
try {
|
|
8
|
+
return await action(client);
|
|
9
|
+
}
|
|
10
|
+
finally {
|
|
11
|
+
await client.close().catch(() => undefined);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
//# sourceMappingURL=daemon-client.js.map
|