@every-env/spiral-cli 0.1.0 → 1.0.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 +54 -9
- package/package.json +5 -14
- package/src/api.ts +82 -474
- package/src/auth.ts +51 -131
- package/src/cli.ts +266 -884
- package/src/config.ts +30 -38
- package/src/output.ts +162 -0
- package/src/types.ts +87 -117
- package/src/attachments/index.ts +0 -174
- package/src/drafts/editor.ts +0 -105
- package/src/drafts/index.ts +0 -208
- package/src/notes/index.ts +0 -130
- package/src/styles/index.ts +0 -45
- package/src/suggestions/diff.ts +0 -33
- package/src/suggestions/index.ts +0 -205
- package/src/suggestions/parser.ts +0 -83
- package/src/tools/renderer.ts +0 -104
- package/src/workspaces/index.ts +0 -55
package/src/auth.ts
CHANGED
|
@@ -1,160 +1,80 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
const SPIRAL_URL = `https://${SPIRAL_DOMAIN}`;
|
|
6
|
-
|
|
7
|
-
// Supported browsers for cookie extraction
|
|
8
|
-
const SUPPORTED_BROWSERS = ["safari", "chrome", "firefox"] as const;
|
|
9
|
-
type Browser = (typeof SUPPORTED_BROWSERS)[number];
|
|
10
|
-
|
|
11
|
-
// In-memory cache (per performance review - avoids Keychain latency)
|
|
12
|
-
let tokenCache: { token: string; expiresAt: number } | null = null;
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Parse JWT expiry without external dependencies
|
|
16
|
-
* @security Never log the full token
|
|
17
|
-
*/
|
|
18
|
-
function getTokenExpiry(token: string): number {
|
|
19
|
-
try {
|
|
20
|
-
const parts = token.split(".");
|
|
21
|
-
const payloadB64 = parts[1];
|
|
22
|
-
if (!payloadB64) return 0;
|
|
23
|
-
const payload = JSON.parse(atob(payloadB64));
|
|
24
|
-
return payload.exp ? payload.exp * 1000 : 0;
|
|
25
|
-
} catch {
|
|
26
|
-
return 0; // Treat as expired if unparseable
|
|
27
|
-
}
|
|
28
|
-
}
|
|
1
|
+
import { input, password } from "@inquirer/prompts";
|
|
2
|
+
import { theme } from "./theme.js";
|
|
3
|
+
import { getStoredAuth, storeAuth, clearAuth } from "./config.js";
|
|
4
|
+
import { AuthenticationError } from "./types.js";
|
|
29
5
|
|
|
30
6
|
/**
|
|
31
|
-
*
|
|
32
|
-
* Note: Clerk tokens are short-lived (60s) and refresh automatically
|
|
7
|
+
* Get auth token. Priority: SPIRAL_TOKEN env → stored PAT → throw.
|
|
33
8
|
*/
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
// Use 5s buffer instead of 60s since Clerk tokens refresh frequently
|
|
38
|
-
const isExpired = expiry === 0 || now >= expiry - 5_000;
|
|
9
|
+
export function getAuthToken(): string {
|
|
10
|
+
const envToken = process.env["SPIRAL_TOKEN"];
|
|
11
|
+
if (envToken) return envToken;
|
|
39
12
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
expiry: expiry ? new Date(expiry).toISOString() : "none",
|
|
43
|
-
now: new Date(now).toISOString(),
|
|
44
|
-
isExpired,
|
|
45
|
-
tokenPreview: `${token.substring(0, 50)}...`,
|
|
46
|
-
});
|
|
47
|
-
}
|
|
13
|
+
const stored = getStoredAuth();
|
|
14
|
+
if (stored?.token) return stored.token;
|
|
48
15
|
|
|
49
|
-
|
|
16
|
+
throw new AuthenticationError(
|
|
17
|
+
"Not authenticated. Run `spiral auth login` or set SPIRAL_TOKEN.",
|
|
18
|
+
);
|
|
50
19
|
}
|
|
51
20
|
|
|
52
21
|
/**
|
|
53
|
-
*
|
|
54
|
-
* Tries Safari, Chrome, Firefox in order
|
|
55
|
-
* @throws AuthenticationError if no session found
|
|
56
|
-
* @security Requires Full Disk Access on macOS Sonoma+
|
|
22
|
+
* Store a PAT token (interactive or via --token flag).
|
|
57
23
|
*/
|
|
58
|
-
export async function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const sessionCookie = cookies.find((c) => c.name === "__session");
|
|
73
|
-
if (sessionCookie?.value) {
|
|
74
|
-
if (process.env.DEBUG) {
|
|
75
|
-
console.debug(`Found session cookie in ${browser}`);
|
|
76
|
-
}
|
|
77
|
-
return sessionCookie.value;
|
|
78
|
-
}
|
|
79
|
-
} catch (error) {
|
|
80
|
-
if (process.env.DEBUG) {
|
|
81
|
-
console.debug(`Failed to extract from ${browser}:`, (error as Error).message);
|
|
82
|
-
}
|
|
83
|
-
// Continue to next browser
|
|
84
|
-
}
|
|
24
|
+
export async function login(tokenArg?: string): Promise<void> {
|
|
25
|
+
const token =
|
|
26
|
+
tokenArg ??
|
|
27
|
+
(await password({
|
|
28
|
+
message: "Paste your Spiral API key (spiral_sk_...):",
|
|
29
|
+
mask: "*",
|
|
30
|
+
validate: (v) =>
|
|
31
|
+
v.startsWith("spiral_sk_") || "Token must start with spiral_sk_",
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
if (!token.startsWith("spiral_sk_")) {
|
|
35
|
+
throw new AuthenticationError("Token must start with spiral_sk_");
|
|
85
36
|
}
|
|
86
37
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
"Note: Full Disk Access may be required in System Preferences.",
|
|
91
|
-
);
|
|
38
|
+
storeAuth(token);
|
|
39
|
+
console.log(theme.success("✓ API key stored successfully."));
|
|
40
|
+
console.log(theme.dim(` Prefix: ${token.slice(0, 14)}...`));
|
|
92
41
|
}
|
|
93
42
|
|
|
94
43
|
/**
|
|
95
|
-
*
|
|
96
|
-
* @security Uses in-memory cache, re-extracts on expiry
|
|
44
|
+
* Show current auth status.
|
|
97
45
|
*/
|
|
98
|
-
export
|
|
99
|
-
|
|
100
|
-
const envToken = process.env.SPIRAL_TOKEN;
|
|
46
|
+
export function showStatus(): void {
|
|
47
|
+
const envToken = process.env["SPIRAL_TOKEN"];
|
|
101
48
|
if (envToken) {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
return envToken;
|
|
49
|
+
console.log(theme.success("✓ Authenticated via SPIRAL_TOKEN env var"));
|
|
50
|
+
console.log(theme.dim(` Prefix: ${envToken.slice(0, 14)}...`));
|
|
51
|
+
return;
|
|
106
52
|
}
|
|
107
53
|
|
|
108
|
-
|
|
109
|
-
if (
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
const token = await extractSpiralAuth();
|
|
115
|
-
|
|
116
|
-
// 3. Check if token is expired
|
|
117
|
-
if (isTokenExpired(token)) {
|
|
118
|
-
throw new AuthenticationError(
|
|
119
|
-
"Session token has expired.\n\n" +
|
|
120
|
-
"To refresh: Open https://app.writewithspiral.com in your browser and refresh the page.\n" +
|
|
121
|
-
"The CLI will automatically pick up the fresh token.\n\n" +
|
|
122
|
-
"Tip: Keep a Spiral tab open while using the CLI for seamless token refresh.",
|
|
123
|
-
);
|
|
54
|
+
const stored = getStoredAuth();
|
|
55
|
+
if (stored?.token) {
|
|
56
|
+
console.log(theme.success("✓ Authenticated via stored API key"));
|
|
57
|
+
console.log(theme.dim(` Prefix: ${stored.tokenPrefix}`));
|
|
58
|
+
console.log(theme.dim(` Stored: ${stored.createdAt}`));
|
|
59
|
+
return;
|
|
124
60
|
}
|
|
125
61
|
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
return token;
|
|
62
|
+
console.log(theme.warning("✗ Not authenticated"));
|
|
63
|
+
console.log(theme.dim(" Run `spiral auth login` or set SPIRAL_TOKEN."));
|
|
129
64
|
}
|
|
130
65
|
|
|
131
66
|
/**
|
|
132
|
-
* Clear
|
|
67
|
+
* Clear stored credentials.
|
|
133
68
|
*/
|
|
134
|
-
export function
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* Sanitize error messages to prevent token leakage
|
|
140
|
-
* @security CRITICAL - tokens must never appear in logs/errors
|
|
141
|
-
*/
|
|
142
|
-
export function sanitizeError(error: Error, token?: string): string {
|
|
143
|
-
let message = error.message;
|
|
144
|
-
if (token) {
|
|
145
|
-
// Remove any occurrence of the full token
|
|
146
|
-
message = message.replace(new RegExp(escapeRegex(token), "g"), "[REDACTED]");
|
|
147
|
-
// Also redact partial tokens (first 20 chars)
|
|
148
|
-
if (token.length > 20) {
|
|
149
|
-
message = message.replace(new RegExp(escapeRegex(token.substring(0, 20)), "g"), "[REDACTED]");
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
return message;
|
|
69
|
+
export function logout(): void {
|
|
70
|
+
clearAuth();
|
|
71
|
+
console.log(theme.success("✓ Stored credentials cleared."));
|
|
153
72
|
}
|
|
154
73
|
|
|
155
74
|
/**
|
|
156
|
-
*
|
|
75
|
+
* Sanitize error messages to prevent token leakage.
|
|
157
76
|
*/
|
|
158
|
-
function
|
|
159
|
-
|
|
77
|
+
export function sanitizeError(error: unknown): string {
|
|
78
|
+
const msg = error instanceof Error ? error.message : String(error);
|
|
79
|
+
return msg.replace(/spiral_sk_[a-zA-Z0-9_-]+/g, "spiral_sk_***");
|
|
160
80
|
}
|