@glassly/cli 2.0.0-dev.0
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 +38 -0
- package/package.json +47 -0
- package/src/api.ts +403 -0
- package/src/config.ts +24 -0
- package/src/credentials.ts +216 -0
- package/src/index.ts +871 -0
- package/src/open-browser.ts +41 -0
- package/src/signing.ts +109 -0
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { homedir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
|
|
5
|
+
const SERVICE = "glassly-cli-v2";
|
|
6
|
+
const LEGACY_NAME = "credentials";
|
|
7
|
+
const GLASSLY_DIR = join(homedir(), ".glassly");
|
|
8
|
+
const CREDS_DIR = join(GLASSLY_DIR, "cli-v2");
|
|
9
|
+
const LEGACY_CREDS_FILE = join(CREDS_DIR, "credentials.json");
|
|
10
|
+
const SIGNING_KEY_SERVICE = "glassly-cli-v2-signing";
|
|
11
|
+
|
|
12
|
+
export type CliJwk = Record<string, unknown> & {
|
|
13
|
+
kty?: string;
|
|
14
|
+
crv?: string;
|
|
15
|
+
x?: string;
|
|
16
|
+
d?: string;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
export interface CliCredentials {
|
|
20
|
+
token: string;
|
|
21
|
+
refreshToken?: string;
|
|
22
|
+
workosUserId: string;
|
|
23
|
+
email: string;
|
|
24
|
+
organizationId?: string | null;
|
|
25
|
+
authenticationMethod?: string;
|
|
26
|
+
coreUrl: string;
|
|
27
|
+
storedAt: string;
|
|
28
|
+
expiresAt?: string;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export interface CliSigningKey {
|
|
32
|
+
coreUrl: string;
|
|
33
|
+
signingKeyId: string;
|
|
34
|
+
publicKeyJwk: CliJwk;
|
|
35
|
+
privateKeyJwk: CliJwk;
|
|
36
|
+
createdAt: string;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export async function saveCredentials(credentials: CliCredentials): Promise<"keychain" | "file"> {
|
|
40
|
+
const payload = JSON.stringify(credentials);
|
|
41
|
+
const name = credentialName(credentials.coreUrl);
|
|
42
|
+
|
|
43
|
+
try {
|
|
44
|
+
if (typeof Bun !== "undefined" && Bun.secrets) {
|
|
45
|
+
await Bun.secrets.set({
|
|
46
|
+
service: SERVICE,
|
|
47
|
+
name,
|
|
48
|
+
value: payload,
|
|
49
|
+
});
|
|
50
|
+
return "keychain";
|
|
51
|
+
}
|
|
52
|
+
} catch {
|
|
53
|
+
// Fall through to file storage.
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
mkdirSync(CREDS_DIR, { recursive: true });
|
|
57
|
+
writeFileSync(credentialsFile(credentials.coreUrl), `${payload}\n`, { mode: 0o600 });
|
|
58
|
+
return "file";
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function loadCredentials(coreUrl?: string): Promise<CliCredentials | null> {
|
|
62
|
+
const targetCoreUrl = coreUrl ? normalizeCoreUrl(coreUrl) : undefined;
|
|
63
|
+
if (process.env.GLASSLY_CLI_TOKEN) {
|
|
64
|
+
return {
|
|
65
|
+
token: process.env.GLASSLY_CLI_TOKEN,
|
|
66
|
+
workosUserId: process.env.GLASSLY_CLI_WORKOS_USER_ID || "unknown",
|
|
67
|
+
email: process.env.GLASSLY_CLI_EMAIL || "unknown",
|
|
68
|
+
organizationId: process.env.GLASSLY_CLI_ORGANIZATION_ID,
|
|
69
|
+
coreUrl: targetCoreUrl || process.env.GLASSLY_CORE_URL || "http://localhost:3000",
|
|
70
|
+
storedAt: new Date().toISOString(),
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const scoped = targetCoreUrl ? await loadScopedCredentials(targetCoreUrl) : null;
|
|
75
|
+
if (scoped) return scoped;
|
|
76
|
+
|
|
77
|
+
try {
|
|
78
|
+
if (typeof Bun !== "undefined" && Bun.secrets) {
|
|
79
|
+
const value = await Bun.secrets.get({
|
|
80
|
+
service: SERVICE,
|
|
81
|
+
name: LEGACY_NAME,
|
|
82
|
+
});
|
|
83
|
+
if (value) {
|
|
84
|
+
const legacy = JSON.parse(value) as CliCredentials;
|
|
85
|
+
if (!targetCoreUrl || normalizeCoreUrl(legacy.coreUrl) === targetCoreUrl) return legacy;
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
} catch {
|
|
89
|
+
// Fall through.
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
if (existsSync(LEGACY_CREDS_FILE)) {
|
|
94
|
+
const legacy = JSON.parse(readFileSync(LEGACY_CREDS_FILE, "utf8")) as CliCredentials;
|
|
95
|
+
if (!targetCoreUrl || normalizeCoreUrl(legacy.coreUrl) === targetCoreUrl) return legacy;
|
|
96
|
+
}
|
|
97
|
+
} catch {
|
|
98
|
+
// Treat corrupt credentials as logged out.
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return null;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
export async function clearCredentials(coreUrl?: string): Promise<void> {
|
|
105
|
+
const targetCoreUrl = coreUrl ? normalizeCoreUrl(coreUrl) : undefined;
|
|
106
|
+
|
|
107
|
+
try {
|
|
108
|
+
if (typeof Bun !== "undefined" && Bun.secrets) {
|
|
109
|
+
await Bun.secrets.set({
|
|
110
|
+
service: SERVICE,
|
|
111
|
+
name: targetCoreUrl ? credentialName(targetCoreUrl) : LEGACY_NAME,
|
|
112
|
+
value: "",
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
} catch {
|
|
116
|
+
// Ignore keychain failures; remove file fallback below.
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
const path = targetCoreUrl ? credentialsFile(targetCoreUrl) : LEGACY_CREDS_FILE;
|
|
120
|
+
if (existsSync(path)) {
|
|
121
|
+
rmSync(path);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export async function saveSigningKey(key: CliSigningKey): Promise<"keychain" | "file"> {
|
|
126
|
+
const payload = JSON.stringify(key);
|
|
127
|
+
const name = credentialName(key.coreUrl);
|
|
128
|
+
|
|
129
|
+
try {
|
|
130
|
+
if (typeof Bun !== "undefined" && Bun.secrets) {
|
|
131
|
+
await Bun.secrets.set({
|
|
132
|
+
service: SIGNING_KEY_SERVICE,
|
|
133
|
+
name,
|
|
134
|
+
value: payload,
|
|
135
|
+
});
|
|
136
|
+
return "keychain";
|
|
137
|
+
}
|
|
138
|
+
} catch {
|
|
139
|
+
// Fall through to file storage.
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
mkdirSync(CREDS_DIR, { recursive: true });
|
|
143
|
+
writeFileSync(signingKeyFile(key.coreUrl), `${payload}\n`, { mode: 0o600 });
|
|
144
|
+
return "file";
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
export async function loadSigningKey(coreUrl: string): Promise<CliSigningKey | null> {
|
|
148
|
+
const targetCoreUrl = normalizeCoreUrl(coreUrl);
|
|
149
|
+
try {
|
|
150
|
+
if (typeof Bun !== "undefined" && Bun.secrets) {
|
|
151
|
+
const value = await Bun.secrets.get({
|
|
152
|
+
service: SIGNING_KEY_SERVICE,
|
|
153
|
+
name: credentialName(targetCoreUrl),
|
|
154
|
+
});
|
|
155
|
+
if (value) return JSON.parse(value) as CliSigningKey;
|
|
156
|
+
}
|
|
157
|
+
} catch {
|
|
158
|
+
// Fall through.
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
try {
|
|
162
|
+
const path = signingKeyFile(targetCoreUrl);
|
|
163
|
+
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8")) as CliSigningKey;
|
|
164
|
+
} catch {
|
|
165
|
+
// Treat corrupt key storage as missing.
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return null;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
async function loadScopedCredentials(coreUrl: string): Promise<CliCredentials | null> {
|
|
172
|
+
try {
|
|
173
|
+
if (typeof Bun !== "undefined" && Bun.secrets) {
|
|
174
|
+
const value = await Bun.secrets.get({
|
|
175
|
+
service: SERVICE,
|
|
176
|
+
name: credentialName(coreUrl),
|
|
177
|
+
});
|
|
178
|
+
if (value) return JSON.parse(value) as CliCredentials;
|
|
179
|
+
}
|
|
180
|
+
} catch {
|
|
181
|
+
// Fall through.
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
const path = credentialsFile(coreUrl);
|
|
186
|
+
if (existsSync(path)) return JSON.parse(readFileSync(path, "utf8")) as CliCredentials;
|
|
187
|
+
} catch {
|
|
188
|
+
// Treat corrupt credentials as logged out.
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
return null;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
function credentialName(coreUrl: string): string {
|
|
195
|
+
return `credentials:${credentialKey(coreUrl)}`;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function credentialsFile(coreUrl: string): string {
|
|
199
|
+
return join(CREDS_DIR, `credentials-${credentialKey(coreUrl)}.json`);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function signingKeyFile(coreUrl: string): string {
|
|
203
|
+
return join(CREDS_DIR, `signing-key-${credentialKey(coreUrl)}.json`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function credentialKey(coreUrl: string): string {
|
|
207
|
+
return Buffer.from(normalizeCoreUrl(coreUrl)).toString("base64url");
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function normalizeCoreUrl(coreUrl: string): string {
|
|
211
|
+
const url = new URL(coreUrl);
|
|
212
|
+
url.pathname = url.pathname.replace(/\/+$/, "");
|
|
213
|
+
url.search = "";
|
|
214
|
+
url.hash = "";
|
|
215
|
+
return url.toString().replace(/\/$/, "");
|
|
216
|
+
}
|