@deploily/deploily-cli 0.1.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/.devcontainer/devcontainer.json +25 -0
- package/.devcontainer/docker-compose.yml +18 -0
- package/.env.example +8 -0
- package/.github/dependabot.yml +12 -0
- package/.github/workflows/publish.yml +39 -0
- package/LICENSE +201 -0
- package/README.md +105 -0
- package/dist/auth/callback.d.ts +3 -0
- package/dist/auth/callback.d.ts.map +1 -0
- package/dist/auth/callback.js +143 -0
- package/dist/auth/callback.js.map +1 -0
- package/dist/auth/index.d.ts +23 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +173 -0
- package/dist/auth/index.js.map +1 -0
- package/dist/commands/auth.d.ts +4 -0
- package/dist/commands/auth.d.ts.map +1 -0
- package/dist/commands/auth.js +90 -0
- package/dist/commands/auth.js.map +1 -0
- package/dist/config/constants.d.ts +22 -0
- package/dist/config/constants.d.ts.map +1 -0
- package/dist/config/constants.js +25 -0
- package/dist/config/constants.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +61 -0
- package/dist/index.js.map +1 -0
- package/dist/storage/credentials.d.ts +47 -0
- package/dist/storage/credentials.d.ts.map +1 -0
- package/dist/storage/credentials.js +160 -0
- package/dist/storage/credentials.js.map +1 -0
- package/dist/utils/pkce.d.ts +5 -0
- package/dist/utils/pkce.d.ts.map +1 -0
- package/dist/utils/pkce.js +30 -0
- package/dist/utils/pkce.js.map +1 -0
- package/package.json +37 -0
- package/pnpm-workspace.yaml +5 -0
- package/src/auth/callback.ts +159 -0
- package/src/auth/index.ts +238 -0
- package/src/commands/auth.ts +114 -0
- package/src/config/constants.ts +28 -0
- package/src/index.ts +69 -0
- package/src/storage/credentials.ts +188 -0
- package/src/types/index.d.ts +35 -0
- package/src/utils/pkce.ts +36 -0
- package/tsconfig.json +24 -0
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
import fs from "fs"
|
|
2
|
+
import path from "path"
|
|
3
|
+
import os from "os"
|
|
4
|
+
import { STORAGE_CONFIG } from "../config/constants.js"
|
|
5
|
+
import { StoredCredentials } from "../types/index.js"
|
|
6
|
+
|
|
7
|
+
// Lazy load keytar - it's optional and may not be available
|
|
8
|
+
let keytar: any = null
|
|
9
|
+
let keytarLoaded = false
|
|
10
|
+
|
|
11
|
+
async function getKeytar() {
|
|
12
|
+
if (keytarLoaded) {
|
|
13
|
+
return keytar
|
|
14
|
+
}
|
|
15
|
+
keytarLoaded = true
|
|
16
|
+
try {
|
|
17
|
+
keytar = await import("keytar")
|
|
18
|
+
} catch (error) {
|
|
19
|
+
// keytar not available, will use fallback
|
|
20
|
+
keytar = null
|
|
21
|
+
}
|
|
22
|
+
return keytar
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
class CredentialStorage {
|
|
26
|
+
private serviceName = STORAGE_CONFIG.SERVICE_NAME
|
|
27
|
+
private accountName = "deploily-auth"
|
|
28
|
+
private configDir: string
|
|
29
|
+
private configFilePath: string
|
|
30
|
+
|
|
31
|
+
constructor() {
|
|
32
|
+
this.configDir = this.expandPath(STORAGE_CONFIG.CONFIG_DIR)
|
|
33
|
+
this.configFilePath = path.join(this.configDir, STORAGE_CONFIG.CONFIG_FILE)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Expand ~ to home directory
|
|
38
|
+
*/
|
|
39
|
+
private expandPath(filepath: string): string {
|
|
40
|
+
if (filepath.startsWith("~")) {
|
|
41
|
+
return path.join(os.homedir(), filepath.slice(1))
|
|
42
|
+
}
|
|
43
|
+
return filepath
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Save credentials using keytar (primary) or fallback to config file
|
|
48
|
+
*/
|
|
49
|
+
async save(credentials: StoredCredentials): Promise<void> {
|
|
50
|
+
const kt = await getKeytar()
|
|
51
|
+
try {
|
|
52
|
+
// Try keytar first
|
|
53
|
+
if (kt) {
|
|
54
|
+
await kt.setPassword(
|
|
55
|
+
this.serviceName,
|
|
56
|
+
this.accountName,
|
|
57
|
+
JSON.stringify(credentials),
|
|
58
|
+
)
|
|
59
|
+
return
|
|
60
|
+
}
|
|
61
|
+
} catch (error) {
|
|
62
|
+
// Fallback to config file
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Fallback to config file
|
|
66
|
+
console.warn("Keytar unavailable, using config file for token storage")
|
|
67
|
+
await this.saveToConfigFile(credentials)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Load credentials using keytar (primary) or fallback to config file
|
|
72
|
+
*/
|
|
73
|
+
async load(): Promise<StoredCredentials | null> {
|
|
74
|
+
const kt = await getKeytar()
|
|
75
|
+
try {
|
|
76
|
+
// Try keytar first
|
|
77
|
+
if (kt) {
|
|
78
|
+
const password = await kt.getPassword(
|
|
79
|
+
this.serviceName,
|
|
80
|
+
this.accountName,
|
|
81
|
+
)
|
|
82
|
+
if (password) {
|
|
83
|
+
return JSON.parse(password)
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
} catch (error) {
|
|
87
|
+
// Fallback to config file
|
|
88
|
+
console.warn("Keytar unavailable, checking config file")
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Try loading from config file
|
|
92
|
+
return await this.loadFromConfigFile()
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Clear credentials from keytar (primary) and config file
|
|
97
|
+
*/
|
|
98
|
+
async clear(): Promise<void> {
|
|
99
|
+
const kt = await getKeytar()
|
|
100
|
+
try {
|
|
101
|
+
// Clear from keytar
|
|
102
|
+
if (kt) {
|
|
103
|
+
await kt.deletePassword(this.serviceName, this.accountName)
|
|
104
|
+
}
|
|
105
|
+
} catch (error) {
|
|
106
|
+
// Fallback to config file
|
|
107
|
+
console.warn("Could not clear keytar credentials")
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Clear from config file
|
|
111
|
+
await this.clearConfigFile()
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Save to config file (fallback method)
|
|
116
|
+
*/
|
|
117
|
+
private async saveToConfigFile(
|
|
118
|
+
credentials: StoredCredentials,
|
|
119
|
+
): Promise<void> {
|
|
120
|
+
try {
|
|
121
|
+
// Ensure config directory exists
|
|
122
|
+
if (!fs.existsSync(this.configDir)) {
|
|
123
|
+
fs.mkdirSync(this.configDir, { recursive: true })
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
fs.writeFileSync(
|
|
127
|
+
this.configFilePath,
|
|
128
|
+
JSON.stringify(credentials, null, 2),
|
|
129
|
+
{ mode: 0o600 }, // Restrict permissions to owner only
|
|
130
|
+
)
|
|
131
|
+
} catch (error) {
|
|
132
|
+
throw new Error(`Failed to save credentials: ${error}`)
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Load from config file (fallback method)
|
|
138
|
+
*/
|
|
139
|
+
private async loadFromConfigFile(): Promise<StoredCredentials | null> {
|
|
140
|
+
try {
|
|
141
|
+
if (!fs.existsSync(this.configFilePath)) {
|
|
142
|
+
return null
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const content = fs.readFileSync(this.configFilePath, "utf-8")
|
|
146
|
+
return JSON.parse(content)
|
|
147
|
+
} catch (error) {
|
|
148
|
+
return null
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Clear config file
|
|
154
|
+
*/
|
|
155
|
+
private async clearConfigFile(): Promise<void> {
|
|
156
|
+
try {
|
|
157
|
+
if (fs.existsSync(this.configFilePath)) {
|
|
158
|
+
fs.unlinkSync(this.configFilePath)
|
|
159
|
+
}
|
|
160
|
+
} catch (error) {
|
|
161
|
+
// Silently fail if file doesn't exist
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Check if credentials exist
|
|
167
|
+
*/
|
|
168
|
+
async exists(): Promise<boolean> {
|
|
169
|
+
const credentials = await this.load()
|
|
170
|
+
return credentials !== null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/**
|
|
174
|
+
* Check if credentials are expired
|
|
175
|
+
*/
|
|
176
|
+
async isExpired(): Promise<boolean> {
|
|
177
|
+
const credentials = await this.load()
|
|
178
|
+
if (!credentials) {
|
|
179
|
+
return true
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// Check if token expires in less than 5 minutes
|
|
183
|
+
const bufferSeconds = 300
|
|
184
|
+
return credentials.expires_at < Date.now() / 1000 + bufferSeconds
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
export const credentialStorage = new CredentialStorage()
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
export interface CallbackResult {
|
|
2
|
+
code: string
|
|
3
|
+
state: string
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// auth
|
|
7
|
+
export interface TokenResponse {
|
|
8
|
+
access_token: string
|
|
9
|
+
refresh_token: string
|
|
10
|
+
expires_in: number
|
|
11
|
+
token_type: string
|
|
12
|
+
scope: string
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
export interface UserInfo {
|
|
16
|
+
email: string
|
|
17
|
+
name: string
|
|
18
|
+
preferred_username: string
|
|
19
|
+
given_name: string
|
|
20
|
+
family_name: string
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AuthContext {
|
|
24
|
+
state: string
|
|
25
|
+
codeVerifier: string
|
|
26
|
+
codeChallenge: string
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
// storage
|
|
30
|
+
export interface StoredCredentials {
|
|
31
|
+
access_token: string
|
|
32
|
+
refresh_token: string
|
|
33
|
+
expires_at: number
|
|
34
|
+
user_email?: string
|
|
35
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import crypto from "crypto"
|
|
2
|
+
|
|
3
|
+
// Generate a cryptographically secure random string
|
|
4
|
+
export function generateRandomString(length: number = 32): string {
|
|
5
|
+
return crypto.randomBytes(length).toString("hex")
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Generate PKCE code verifier
|
|
9
|
+
export function generateCodeVerifier(): string {
|
|
10
|
+
const length = 128
|
|
11
|
+
const charset =
|
|
12
|
+
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-._~"
|
|
13
|
+
let result = ""
|
|
14
|
+
const randomValues = crypto.getRandomValues(new Uint8Array(length))
|
|
15
|
+
|
|
16
|
+
for (let i = 0; i < length; i++) {
|
|
17
|
+
result += charset[randomValues[i] % charset.length]
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
return result
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// Generate PKCE code challenge from verifier
|
|
24
|
+
export function generateCodeChallenge(codeVerifier: string): string {
|
|
25
|
+
const hash = crypto.createHash("sha256").update(codeVerifier).digest()
|
|
26
|
+
return hash
|
|
27
|
+
.toString("base64")
|
|
28
|
+
.replace(/\+/g, "-")
|
|
29
|
+
.replace(/\//g, "_")
|
|
30
|
+
.replace(/=+$/, "")
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Generate state parameter for CSRF protection
|
|
34
|
+
export function generateState(): string {
|
|
35
|
+
return generateRandomString(32)
|
|
36
|
+
}
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2020",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"lib": ["ES2020"],
|
|
6
|
+
"moduleResolution": "bundler",
|
|
7
|
+
"types": ["node"],
|
|
8
|
+
"esModuleInterop": true,
|
|
9
|
+
"jsx": "react-jsx",
|
|
10
|
+
"jsxImportSource": "hono/jsx",
|
|
11
|
+
"allowSyntheticDefaultImports": true,
|
|
12
|
+
"strict": true,
|
|
13
|
+
"skipLibCheck": true,
|
|
14
|
+
"forceConsistentCasingInFileNames": true,
|
|
15
|
+
"resolveJsonModule": true,
|
|
16
|
+
"declaration": true,
|
|
17
|
+
"declarationMap": true,
|
|
18
|
+
"sourceMap": true,
|
|
19
|
+
"outDir": "./dist",
|
|
20
|
+
"rootDir": "./src"
|
|
21
|
+
},
|
|
22
|
+
"include": ["src/**/*"],
|
|
23
|
+
"exclude": ["node_modules", "dist", "**/*.test.ts"]
|
|
24
|
+
}
|