@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.
Files changed (41) hide show
  1. package/dist/commands/hub/authority.d.ts +17 -0
  2. package/dist/commands/hub/authority.js +18 -0
  3. package/dist/commands/hub/client.d.ts +55 -3
  4. package/dist/commands/hub/client.js +158 -38
  5. package/dist/commands/hub/connect.d.ts +21 -0
  6. package/dist/commands/hub/connect.js +34 -0
  7. package/dist/commands/hub/credentials.d.ts +21 -0
  8. package/dist/commands/hub/credentials.js +143 -0
  9. package/dist/commands/hub/daemon-client.d.ts +27 -0
  10. package/dist/commands/hub/daemon-client.js +14 -0
  11. package/dist/commands/hub/deploy-input.js +28 -28
  12. package/dist/commands/hub/deploy.d.ts +23 -3
  13. package/dist/commands/hub/deploy.js +51 -42
  14. package/dist/commands/hub/disconnect.d.ts +16 -0
  15. package/dist/commands/hub/disconnect.js +24 -0
  16. package/dist/commands/hub/error.d.ts +1 -1
  17. package/dist/commands/hub/error.js +2 -2
  18. package/dist/commands/hub/help.d.ts +3 -0
  19. package/dist/commands/hub/help.js +5 -0
  20. package/dist/commands/hub/index.d.ts +15 -25
  21. package/dist/commands/hub/index.js +64 -74
  22. package/dist/commands/hub/{device-authorization.d.ts → login-flow.d.ts} +12 -15
  23. package/dist/commands/hub/login-flow.js +78 -0
  24. package/dist/commands/hub/login.d.ts +21 -0
  25. package/dist/commands/hub/login.js +34 -0
  26. package/dist/commands/hub/logout.d.ts +31 -0
  27. package/dist/commands/hub/logout.js +65 -0
  28. package/dist/commands/hub/origin.d.ts +2 -0
  29. package/dist/commands/hub/origin.js +27 -0
  30. package/dist/commands/hub/projects.d.ts +24 -0
  31. package/dist/commands/hub/projects.js +49 -0
  32. package/dist/commands/hub/reporter.d.ts +10 -0
  33. package/dist/commands/hub/reporter.js +11 -0
  34. package/dist/commands/hub/status-output.d.ts +13 -0
  35. package/dist/commands/hub/status-output.js +30 -0
  36. package/dist/commands/schedule/types.d.ts +1 -4
  37. package/dist/output/types.d.ts +1 -1
  38. package/package.json +4 -4
  39. package/dist/commands/hub/cloud-device-authorization.d.ts +0 -45
  40. package/dist/commands/hub/cloud-device-authorization.js +0 -92
  41. 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.ZodBoolean;
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
- interface InstallHubConfigurationInput {
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 function installHubConfiguration(input: InstallHubConfigurationInput): Promise<HubInstallResult>;
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 { HubDeployError } from "./error.js";
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.boolean(),
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 async function installHubConfiguration(input) {
29
- let response;
30
- try {
31
- response = await fetch(`${input.origin}/api/v1/configurations/install`, {
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
- headers: {
34
- authorization: `Bearer ${input.apiKey}`,
35
- "content-type": "application/json",
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
- catch {
47
- throw new HubDeployError("HUB_NETWORK_ERROR", `Could not reach Paseo Hub at ${input.origin}. Check the Hub URL and network connection.`);
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
- if (response.status !== 201) {
50
- throw await deploymentFailure(response, input.apiKey);
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
- try {
53
- return installResponseSchema.parse(await response.json());
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
- catch {
56
- throw new HubDeployError("HUB_INVALID_RESPONSE", "Hub returned a malformed deployment response.");
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
- async function deploymentFailure(response, apiKey) {
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 HubDeployError("HUB_REQUEST_FAILED", `Hub deployment failed with HTTP ${response.status}.`);
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 HubDeployError("HUB_INVALID_RESPONSE", `Hub returned malformed problem details for HTTP ${response.status}.`);
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 HubDeployError("HUB_INVALID_RESPONSE", `Hub returned nonconforming problem details for HTTP ${response.status}.`);
197
+ return new HubCommandError("HUB_INVALID_RESPONSE", `Hub returned nonconforming problem details for HTTP ${response.status}.`);
75
198
  }
76
- const title = parsed.data.title ?? `Hub deployment failed with HTTP ${response.status}`;
77
- const detail = parsed.data.detail;
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 HubDeployError(code, redactSecret(message, apiKey), details === undefined ? undefined : redactSecret(details, apiKey));
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