@gpc-cli/auth 0.9.5 → 0.9.7
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 +143 -28
- package/dist/index.js +15 -3
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# @gpc-cli/auth
|
|
2
2
|
|
|
3
|
-
Authentication
|
|
3
|
+
Authentication for Google Play Developer API -- service account, OAuth, and Application Default Credentials.
|
|
4
4
|
|
|
5
5
|
## Install
|
|
6
6
|
|
|
@@ -8,55 +8,170 @@ Authentication strategies for Google Play Developer API. Supports service accoun
|
|
|
8
8
|
npm install @gpc-cli/auth
|
|
9
9
|
```
|
|
10
10
|
|
|
11
|
-
##
|
|
11
|
+
## Quick Start
|
|
12
12
|
|
|
13
13
|
```typescript
|
|
14
|
-
import { resolveAuth
|
|
14
|
+
import { resolveAuth } from "@gpc-cli/auth";
|
|
15
|
+
import { createApiClient } from "@gpc-cli/api";
|
|
16
|
+
|
|
17
|
+
const auth = await resolveAuth({
|
|
18
|
+
serviceAccountPath: "./service-account.json",
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
const client = createApiClient({ auth });
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
## Authentication Methods
|
|
25
|
+
|
|
26
|
+
### Service Account (file path)
|
|
27
|
+
|
|
28
|
+
The most common method for CI/CD and automation. Download a service account JSON key from the Google Cloud Console.
|
|
29
|
+
|
|
30
|
+
```typescript
|
|
31
|
+
import { resolveAuth } from "@gpc-cli/auth";
|
|
32
|
+
|
|
33
|
+
const auth = await resolveAuth({
|
|
34
|
+
serviceAccountPath: "./service-account.json",
|
|
35
|
+
});
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
### Service Account (JSON string)
|
|
39
|
+
|
|
40
|
+
Pass the key contents directly -- useful for environment variables and secrets managers.
|
|
41
|
+
|
|
42
|
+
```typescript
|
|
43
|
+
import { resolveAuth } from "@gpc-cli/auth";
|
|
15
44
|
|
|
16
|
-
// Auto-resolve from config
|
|
17
45
|
const auth = await resolveAuth({
|
|
18
|
-
|
|
46
|
+
serviceAccountJson: process.env.SERVICE_ACCOUNT_KEY,
|
|
19
47
|
});
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
### Service Account (manual creation)
|
|
51
|
+
|
|
52
|
+
For more control, load and create the auth client in two steps:
|
|
20
53
|
|
|
21
|
-
|
|
22
|
-
|
|
54
|
+
```typescript
|
|
55
|
+
import { loadServiceAccountKey, createServiceAccountAuth } from "@gpc-cli/auth";
|
|
56
|
+
|
|
57
|
+
const key = await loadServiceAccountKey("./service-account.json");
|
|
23
58
|
const auth = createServiceAccountAuth(key);
|
|
24
59
|
|
|
25
|
-
//
|
|
26
|
-
|
|
27
|
-
|
|
60
|
+
console.log(auth.getClientEmail()); // your-sa@project.iam.gserviceaccount.com
|
|
61
|
+
console.log(auth.getProjectId()); // your-gcp-project-id
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`loadServiceAccountKey` accepts a file path or a raw JSON string, and validates the key structure before returning.
|
|
65
|
+
|
|
66
|
+
### Application Default Credentials
|
|
67
|
+
|
|
68
|
+
Works automatically in GCP-hosted environments (Cloud Build, Cloud Run, GKE) and locally after running `gcloud auth application-default login`.
|
|
69
|
+
|
|
70
|
+
```typescript
|
|
71
|
+
import { resolveAuth } from "@gpc-cli/auth";
|
|
72
|
+
|
|
73
|
+
// No options needed -- resolveAuth falls back to ADC automatically
|
|
74
|
+
const auth = await resolveAuth();
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Environment Variables
|
|
78
|
+
|
|
79
|
+
`resolveAuth` checks these environment variables when no explicit options are provided:
|
|
80
|
+
|
|
81
|
+
| Variable | Description |
|
|
82
|
+
| --- | --- |
|
|
83
|
+
| `GPC_SERVICE_ACCOUNT` | File path or raw JSON string of a service account key |
|
|
84
|
+
| `GOOGLE_APPLICATION_CREDENTIALS` | Standard GCP credential path (also used by ADC) |
|
|
85
|
+
|
|
86
|
+
### Resolution Order
|
|
87
|
+
|
|
88
|
+
`resolveAuth` tries credentials in this order:
|
|
89
|
+
|
|
90
|
+
1. `serviceAccountJson` option (inline JSON)
|
|
91
|
+
2. `serviceAccountPath` option (file path)
|
|
92
|
+
3. `GPC_SERVICE_ACCOUNT` environment variable
|
|
93
|
+
4. `GOOGLE_APPLICATION_CREDENTIALS` environment variable
|
|
94
|
+
5. Application Default Credentials
|
|
95
|
+
|
|
96
|
+
The first method that succeeds is used.
|
|
97
|
+
|
|
98
|
+
## Token Caching
|
|
99
|
+
|
|
100
|
+
Access tokens are cached in memory and reused until they expire. This avoids redundant token requests when making multiple API calls.
|
|
101
|
+
|
|
102
|
+
To clear the cache manually:
|
|
103
|
+
|
|
104
|
+
```typescript
|
|
105
|
+
import { clearTokenCache } from "@gpc-cli/auth";
|
|
106
|
+
|
|
107
|
+
clearTokenCache();
|
|
28
108
|
```
|
|
29
109
|
|
|
30
|
-
##
|
|
110
|
+
## AuthClient Interface
|
|
31
111
|
|
|
32
|
-
|
|
33
|
-
| --------------- | ------------------ | ------------------------------------ |
|
|
34
|
-
| Service account | CI/CD, automation | `serviceAccount` path or JSON string |
|
|
35
|
-
| OAuth 2.0 | Local development | Interactive login flow |
|
|
36
|
-
| ADC | GCP-hosted runners | `GPC_USE_ADC=1` or `--adc` flag |
|
|
37
|
-
| Env var | Docker, ephemeral | `GPC_SERVICE_ACCOUNT` env var |
|
|
112
|
+
Every auth method returns an `AuthClient`:
|
|
38
113
|
|
|
39
|
-
|
|
114
|
+
```typescript
|
|
115
|
+
interface AuthClient {
|
|
116
|
+
getAccessToken(): Promise<string>;
|
|
117
|
+
getProjectId(): string | undefined;
|
|
118
|
+
getClientEmail(): string;
|
|
119
|
+
}
|
|
120
|
+
```
|
|
40
121
|
|
|
41
|
-
|
|
122
|
+
This is the interface that `@gpc-cli/api` expects. You can also implement it yourself for custom auth flows:
|
|
42
123
|
|
|
43
|
-
|
|
124
|
+
```typescript
|
|
125
|
+
import { createApiClient } from "@gpc-cli/api";
|
|
44
126
|
|
|
45
|
-
|
|
127
|
+
const client = createApiClient({
|
|
128
|
+
auth: {
|
|
129
|
+
async getAccessToken() {
|
|
130
|
+
return fetchTokenFromVault();
|
|
131
|
+
},
|
|
132
|
+
getProjectId() { return "my-project"; },
|
|
133
|
+
getClientEmail() { return "custom@example.com"; },
|
|
134
|
+
},
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## Type Exports
|
|
46
139
|
|
|
47
|
-
|
|
140
|
+
```typescript
|
|
141
|
+
import type { AuthOptions, AuthClient, ServiceAccountKey } from "@gpc-cli/auth";
|
|
142
|
+
```
|
|
48
143
|
|
|
49
|
-
|
|
144
|
+
## Error Handling
|
|
50
145
|
|
|
51
|
-
|
|
146
|
+
Auth failures throw `AuthError` with a code and actionable suggestion:
|
|
147
|
+
|
|
148
|
+
```typescript
|
|
149
|
+
import { AuthError } from "@gpc-cli/auth";
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const auth = await resolveAuth();
|
|
153
|
+
} catch (error) {
|
|
154
|
+
if (error instanceof AuthError) {
|
|
155
|
+
console.error(error.code); // e.g. "AUTH_NO_CREDENTIALS"
|
|
156
|
+
console.error(error.suggestion); // step-by-step fix
|
|
157
|
+
console.error(error.toJSON()); // structured error object
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
```
|
|
52
161
|
|
|
53
|
-
|
|
162
|
+
Error codes:
|
|
54
163
|
|
|
55
|
-
|
|
164
|
+
| Code | Meaning |
|
|
165
|
+
| --- | --- |
|
|
166
|
+
| `AUTH_NO_CREDENTIALS` | No credentials found via any method |
|
|
167
|
+
| `AUTH_INVALID_KEY` | Service account key is malformed or missing required fields |
|
|
168
|
+
| `AUTH_TOKEN_FAILED` | Token generation failed (expired key, wrong permissions, network) |
|
|
169
|
+
| `AUTH_FILE_NOT_FOUND` | Service account file path does not exist |
|
|
56
170
|
|
|
57
|
-
##
|
|
171
|
+
## Documentation
|
|
58
172
|
|
|
59
|
-
|
|
173
|
+
- [Full documentation](https://yasserstudio.github.io/gpc/)
|
|
174
|
+
- [SDK usage guide](https://yasserstudio.github.io/gpc/advanced/sdk-usage.html)
|
|
60
175
|
|
|
61
176
|
## License
|
|
62
177
|
|
package/dist/index.js
CHANGED
|
@@ -37,13 +37,21 @@ var memoryCache = /* @__PURE__ */ new Map();
|
|
|
37
37
|
var inflightRefresh = /* @__PURE__ */ new Map();
|
|
38
38
|
function getCachePath(cacheDir) {
|
|
39
39
|
if (!isAbsolute(cacheDir)) {
|
|
40
|
-
throw new
|
|
40
|
+
throw new AuthError(
|
|
41
|
+
"Cache directory must be an absolute path",
|
|
42
|
+
"AUTH_CACHE_INVALID",
|
|
43
|
+
"Provide an absolute path for the token cache directory (e.g., /home/user/.cache/gpc)."
|
|
44
|
+
);
|
|
41
45
|
}
|
|
42
46
|
return join(cacheDir, CACHE_FILE);
|
|
43
47
|
}
|
|
44
48
|
function validateCacheKey(email) {
|
|
45
49
|
if (!SAFE_CACHE_KEY.test(email)) {
|
|
46
|
-
throw new
|
|
50
|
+
throw new AuthError(
|
|
51
|
+
"Invalid cache key: must be a valid email address",
|
|
52
|
+
"AUTH_CACHE_INVALID",
|
|
53
|
+
"The cache key must be a valid service account email address (e.g., my-sa@project.iam.gserviceaccount.com)."
|
|
54
|
+
);
|
|
47
55
|
}
|
|
48
56
|
}
|
|
49
57
|
async function readCache(cacheDir) {
|
|
@@ -219,7 +227,11 @@ function createServiceAccountAuth(key, cachePath) {
|
|
|
219
227
|
return await acquireToken(key.client_email, cachePath, async () => {
|
|
220
228
|
const { token } = await jwtClient.getAccessToken();
|
|
221
229
|
if (!token) {
|
|
222
|
-
throw new
|
|
230
|
+
throw new AuthError(
|
|
231
|
+
"Token response was empty.",
|
|
232
|
+
"AUTH_TOKEN_FAILED",
|
|
233
|
+
"Verify that the service account key is valid and not expired. Check that the private key has not been revoked."
|
|
234
|
+
);
|
|
223
235
|
}
|
|
224
236
|
return { token, expiresInSeconds: TOKEN_EXPIRY_SECONDS };
|
|
225
237
|
});
|
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/resolve.ts","../src/errors.ts","../src/service-account.ts","../src/token-cache.ts"],"sourcesContent":["import { GoogleAuth } from \"google-auth-library\";\nimport { AuthError } from \"./errors.js\";\nimport { createServiceAccountAuth, loadServiceAccountKey } from \"./service-account.js\";\nimport { acquireToken } from \"./token-cache.js\";\nimport type { AuthClient, AuthOptions } from \"./types.js\";\n\nconst ANDROID_PUBLISHER_SCOPE = \"https://www.googleapis.com/auth/androidpublisher\";\n\nasync function tryApplicationDefaultCredentials(\n cachePath?: string,\n): Promise<AuthClient | null> {\n try {\n const auth = new GoogleAuth({\n scopes: [ANDROID_PUBLISHER_SCOPE],\n });\n\n const client = await auth.getClient();\n const projectId = await auth.getProjectId().catch(() => undefined);\n const email = (client as { email?: string }).email ?? \"adc-default\";\n\n return {\n async getAccessToken(): Promise<string> {\n return acquireToken(email, cachePath, async () => {\n const { token } = await client.getAccessToken();\n if (!token) {\n throw new AuthError(\n \"Application Default Credentials returned an empty token.\",\n \"AUTH_TOKEN_FAILED\",\n \"Verify your ADC configuration with: gcloud auth application-default print-access-token\",\n );\n }\n return { token, expiresInSeconds: 3600 };\n });\n },\n\n getProjectId(): string | undefined {\n return projectId ?? undefined;\n },\n\n getClientEmail(): string {\n return email;\n },\n };\n } catch {\n return null;\n }\n}\n\nexport async function resolveAuth(options?: AuthOptions): Promise<AuthClient> {\n // 1. Explicit options\n if (options?.serviceAccountJson) {\n const key = await loadServiceAccountKey(options.serviceAccountJson);\n return createServiceAccountAuth(key, options?.cachePath);\n }\n\n if (options?.serviceAccountPath) {\n const key = await loadServiceAccountKey(options.serviceAccountPath);\n return createServiceAccountAuth(key, options?.cachePath);\n }\n\n // 2. GPC_SERVICE_ACCOUNT environment variable\n const envValue = process.env[\"GPC_SERVICE_ACCOUNT\"];\n if (envValue) {\n const key = await loadServiceAccountKey(envValue);\n return createServiceAccountAuth(key, options?.cachePath);\n }\n\n // 3. GOOGLE_APPLICATION_CREDENTIALS environment variable\n const gacPath = process.env[\"GOOGLE_APPLICATION_CREDENTIALS\"];\n if (gacPath) {\n try {\n const key = await loadServiceAccountKey(gacPath);\n return createServiceAccountAuth(key, options?.cachePath);\n } catch {\n // Fall through to ADC which also reads GOOGLE_APPLICATION_CREDENTIALS\n }\n }\n\n // 4. Application Default Credentials\n const adcClient = await tryApplicationDefaultCredentials(options?.cachePath);\n if (adcClient) {\n return adcClient;\n }\n\n throw new AuthError(\n \"No credentials found. Could not authenticate with the Google Play Developer API.\",\n \"AUTH_NO_CREDENTIALS\",\n [\n \"Provide credentials using one of these methods:\",\n \" 1. Pass serviceAccountPath or serviceAccountJson in options\",\n \" 2. Set the GPC_SERVICE_ACCOUNT environment variable to a file path or raw JSON\",\n \" 3. Set GOOGLE_APPLICATION_CREDENTIALS to a service account key file\",\n \" 4. Configure Application Default Credentials: gcloud auth application-default login\",\n ].join(\"\\n\"),\n );\n}\n","export class AuthError extends Error {\n public readonly exitCode = 3;\n constructor(\n message: string,\n public readonly code: string,\n public readonly suggestion?: string,\n ) {\n super(message);\n this.name = \"AuthError\";\n }\n toJSON() {\n return {\n success: false,\n error: {\n code: this.code,\n message: this.message,\n suggestion: this.suggestion,\n },\n };\n }\n}\n","import { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { JWT } from \"google-auth-library\";\nimport { AuthError } from \"./errors.js\";\nimport { acquireToken } from \"./token-cache.js\";\nimport type { AuthClient, ServiceAccountKey } from \"./types.js\";\n\nconst ANDROID_PUBLISHER_SCOPE = \"https://www.googleapis.com/auth/androidpublisher\";\n\nconst REQUIRED_FIELDS: readonly (keyof ServiceAccountKey)[] = [\n \"type\",\n \"private_key\",\n \"client_email\",\n];\n\nfunction validateServiceAccountKey(data: unknown): asserts data is ServiceAccountKey {\n if (typeof data !== \"object\" || data === null) {\n throw new AuthError(\n \"Service account key must be a JSON object.\",\n \"AUTH_INVALID_KEY\",\n \"Ensure the file contains valid JSON with the required service account fields.\",\n );\n }\n\n const record = data as Record<string, unknown>;\n\n for (const field of REQUIRED_FIELDS) {\n if (typeof record[field] !== \"string\" || record[field] === \"\") {\n throw new AuthError(\n `Service account key is missing required field: \"${field}\".`,\n \"AUTH_INVALID_KEY\",\n `Download a fresh service account key from the Google Cloud Console. The key must include: ${REQUIRED_FIELDS.join(\", \")}.`,\n );\n }\n }\n\n if (record[\"type\"] !== \"service_account\") {\n throw new AuthError(\n `Invalid key type \"${String(record[\"type\"])}\". Expected \"service_account\".`,\n \"AUTH_INVALID_KEY\",\n \"Ensure you are using a service account key, not an OAuth client or API key.\",\n );\n }\n}\n\nexport async function loadServiceAccountKey(pathOrJson: string): Promise<ServiceAccountKey> {\n let raw: string;\n\n const trimmed = pathOrJson.trim();\n\n if (trimmed.startsWith(\"{\")) {\n raw = trimmed;\n } else {\n const absolutePath = resolve(trimmed);\n try {\n raw = await readFile(absolutePath, \"utf-8\");\n } catch (err) {\n const code =\n err instanceof Error && \"code\" in err && err.code === \"ENOENT\"\n ? \"AUTH_FILE_NOT_FOUND\"\n : \"AUTH_INVALID_KEY\";\n\n throw new AuthError(\n `Failed to read service account key file: ${absolutePath}`,\n code,\n code === \"AUTH_FILE_NOT_FOUND\"\n ? `File not found. Check that the path is correct: ${absolutePath}`\n : \"Ensure the file is readable and contains valid JSON.\",\n );\n }\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new AuthError(\n \"Failed to parse service account key as JSON.\",\n \"AUTH_INVALID_KEY\",\n \"Ensure the value is valid JSON. If passing a file path, check that the path points to a JSON file.\",\n );\n }\n\n validateServiceAccountKey(parsed);\n return parsed;\n}\n\nconst TOKEN_EXPIRY_SECONDS = 3600; // Google OAuth2 tokens expire in 1 hour\n\nexport function createServiceAccountAuth(key: ServiceAccountKey, cachePath?: string): AuthClient {\n const jwtClient = new JWT({\n email: key.client_email,\n key: key.private_key,\n scopes: [ANDROID_PUBLISHER_SCOPE],\n });\n\n return {\n async getAccessToken(): Promise<string> {\n try {\n return await acquireToken(key.client_email, cachePath, async () => {\n const { token } = await jwtClient.getAccessToken();\n if (!token) {\n throw new Error(\"Token response was empty.\");\n }\n return { token, expiresInSeconds: TOKEN_EXPIRY_SECONDS };\n });\n } catch (err) {\n if (err instanceof AuthError) throw err;\n const rawMsg = err instanceof Error ? err.message : String(err);\n const safeMsg = rawMsg.length > 150 ? rawMsg.slice(0, 150) + \"...\" : rawMsg;\n throw new AuthError(\n `Failed to obtain access token: ${safeMsg}`,\n \"AUTH_TOKEN_FAILED\",\n \"Verify that the service account key is valid and not expired. Check that the private key has not been revoked.\",\n );\n }\n },\n\n getProjectId(): string | undefined {\n return key.project_id || undefined;\n },\n\n getClientEmail(): string {\n return key.client_email;\n },\n };\n}\n","import { chmod, mkdir, readFile, writeFile, rename, unlink } from \"node:fs/promises\";\nimport { dirname, join, isAbsolute } from \"node:path\";\n\nexport interface TokenCacheEntry {\n token: string;\n expiresAt: number;\n}\n\nexport type TokenCache = Record<string, TokenCacheEntry>;\n\nconst CACHE_FILE = \"token-cache.json\";\nconst SAFETY_MARGIN_MS = 5 * 60 * 1000; // 5 minutes\n\n// Email must look like a service account email — no path separators or special chars\nconst SAFE_CACHE_KEY = /^[a-zA-Z0-9._%+@-]+$/;\n\n// In-memory cache layer — avoids filesystem I/O on every token request\nconst memoryCache = new Map<string, TokenCacheEntry>();\n\n// Mutex: one in-flight token refresh per email, deduplicates concurrent callers\nconst inflightRefresh = new Map<string, Promise<string>>();\n\nfunction getCachePath(cacheDir: string): string {\n if (!isAbsolute(cacheDir)) {\n throw new Error(\"Cache directory must be an absolute path\");\n }\n return join(cacheDir, CACHE_FILE);\n}\n\nfunction validateCacheKey(email: string): void {\n if (!SAFE_CACHE_KEY.test(email)) {\n throw new Error(\"Invalid cache key: must be a valid email address\");\n }\n}\n\nasync function readCache(cacheDir: string): Promise<TokenCache> {\n try {\n const content = await readFile(getCachePath(cacheDir), \"utf-8\");\n return JSON.parse(content) as TokenCache;\n } catch {\n return {};\n }\n}\n\nasync function writeCache(cacheDir: string, cache: TokenCache): Promise<void> {\n const cachePath = getCachePath(cacheDir);\n const tmpPath = cachePath + \".tmp\";\n\n const cacheParent = dirname(cachePath);\n await mkdir(cacheParent, { recursive: true });\n // Restrict cache directory to owner-only (0o700)\n await chmod(cacheParent, 0o700).catch(() => {});\n await writeFile(tmpPath, JSON.stringify(cache, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n await rename(tmpPath, cachePath);\n}\n\nfunction isEntryValid(entry: TokenCacheEntry): boolean {\n return Date.now() < entry.expiresAt - SAFETY_MARGIN_MS;\n}\n\nexport async function getCachedToken(cacheDir: string, email: string): Promise<string | null> {\n validateCacheKey(email);\n\n // Check in-memory cache first — no I/O\n const memEntry = memoryCache.get(email);\n if (memEntry && isEntryValid(memEntry)) {\n return memEntry.token;\n }\n\n // Fall back to filesystem cache\n const cache = await readCache(cacheDir);\n const entry = cache[email];\n\n if (!entry) return null;\n\n if (!isEntryValid(entry)) {\n return null;\n }\n\n // Populate in-memory cache from disk\n memoryCache.set(email, entry);\n return entry.token;\n}\n\nexport async function setCachedToken(\n cacheDir: string,\n email: string,\n token: string,\n expiresInSeconds: number,\n): Promise<void> {\n validateCacheKey(email);\n const entry: TokenCacheEntry = {\n token,\n expiresAt: Date.now() + expiresInSeconds * 1000,\n };\n\n // Update in-memory cache immediately\n memoryCache.set(email, entry);\n\n // Persist to disk\n const cache = await readCache(cacheDir);\n cache[email] = entry;\n await writeCache(cacheDir, cache);\n}\n\nexport async function clearTokenCache(cacheDir: string, email?: string): Promise<void> {\n if (email) {\n memoryCache.delete(email);\n const cache = await readCache(cacheDir);\n const updated = Object.fromEntries(Object.entries(cache).filter(([key]) => key !== email));\n await writeCache(cacheDir, updated);\n } else {\n memoryCache.clear();\n try {\n await unlink(getCachePath(cacheDir));\n } catch {\n // File doesn't exist — nothing to clear\n }\n }\n}\n\n/**\n * Acquire a token with mutex protection.\n * If another caller is already refreshing for this email, waits for that result\n * instead of starting a duplicate refresh.\n */\nexport async function acquireToken(\n email: string,\n cacheDir: string | undefined,\n refresh: () => Promise<{ token: string; expiresInSeconds: number }>,\n): Promise<string> {\n // Fast path: in-memory hit\n const memEntry = memoryCache.get(email);\n if (memEntry && isEntryValid(memEntry)) {\n return memEntry.token;\n }\n\n // Disk cache check (only if cacheDir provided)\n if (cacheDir) {\n const cached = await getCachedToken(cacheDir, email);\n if (cached) return cached;\n }\n\n // Deduplicate concurrent refreshes for the same email\n const inflight = inflightRefresh.get(email);\n if (inflight) return inflight;\n\n const refreshPromise = (async () => {\n const { token, expiresInSeconds } = await refresh();\n\n // Update both memory and disk caches\n const entry: TokenCacheEntry = {\n token,\n expiresAt: Date.now() + expiresInSeconds * 1000,\n };\n memoryCache.set(email, entry);\n\n if (cacheDir) {\n await setCachedToken(cacheDir, email, token, expiresInSeconds).catch(() => {});\n }\n\n return token;\n })();\n\n inflightRefresh.set(email, refreshPromise);\n\n try {\n return await refreshPromise;\n } finally {\n inflightRefresh.delete(email);\n }\n}\n\n/** Reset in-memory state. Exported for testing only. */\nexport function _resetMemoryCache(): void {\n memoryCache.clear();\n inflightRefresh.clear();\n}\n"],"mappings":"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","names":["readFile","readFile","ANDROID_PUBLISHER_SCOPE"]}
|
|
1
|
+
{"version":3,"sources":["../src/resolve.ts","../src/errors.ts","../src/service-account.ts","../src/token-cache.ts"],"sourcesContent":["import { GoogleAuth } from \"google-auth-library\";\nimport { AuthError } from \"./errors.js\";\nimport { createServiceAccountAuth, loadServiceAccountKey } from \"./service-account.js\";\nimport { acquireToken } from \"./token-cache.js\";\nimport type { AuthClient, AuthOptions } from \"./types.js\";\n\nconst ANDROID_PUBLISHER_SCOPE = \"https://www.googleapis.com/auth/androidpublisher\";\n\nasync function tryApplicationDefaultCredentials(\n cachePath?: string,\n): Promise<AuthClient | null> {\n try {\n const auth = new GoogleAuth({\n scopes: [ANDROID_PUBLISHER_SCOPE],\n });\n\n const client = await auth.getClient();\n const projectId = await auth.getProjectId().catch(() => undefined);\n const email = (client as { email?: string }).email ?? \"adc-default\";\n\n return {\n async getAccessToken(): Promise<string> {\n return acquireToken(email, cachePath, async () => {\n const { token } = await client.getAccessToken();\n if (!token) {\n throw new AuthError(\n \"Application Default Credentials returned an empty token.\",\n \"AUTH_TOKEN_FAILED\",\n \"Verify your ADC configuration with: gcloud auth application-default print-access-token\",\n );\n }\n return { token, expiresInSeconds: 3600 };\n });\n },\n\n getProjectId(): string | undefined {\n return projectId ?? undefined;\n },\n\n getClientEmail(): string {\n return email;\n },\n };\n } catch {\n return null;\n }\n}\n\nexport async function resolveAuth(options?: AuthOptions): Promise<AuthClient> {\n // 1. Explicit options\n if (options?.serviceAccountJson) {\n const key = await loadServiceAccountKey(options.serviceAccountJson);\n return createServiceAccountAuth(key, options?.cachePath);\n }\n\n if (options?.serviceAccountPath) {\n const key = await loadServiceAccountKey(options.serviceAccountPath);\n return createServiceAccountAuth(key, options?.cachePath);\n }\n\n // 2. GPC_SERVICE_ACCOUNT environment variable\n const envValue = process.env[\"GPC_SERVICE_ACCOUNT\"];\n if (envValue) {\n const key = await loadServiceAccountKey(envValue);\n return createServiceAccountAuth(key, options?.cachePath);\n }\n\n // 3. GOOGLE_APPLICATION_CREDENTIALS environment variable\n const gacPath = process.env[\"GOOGLE_APPLICATION_CREDENTIALS\"];\n if (gacPath) {\n try {\n const key = await loadServiceAccountKey(gacPath);\n return createServiceAccountAuth(key, options?.cachePath);\n } catch {\n // Fall through to ADC which also reads GOOGLE_APPLICATION_CREDENTIALS\n }\n }\n\n // 4. Application Default Credentials\n const adcClient = await tryApplicationDefaultCredentials(options?.cachePath);\n if (adcClient) {\n return adcClient;\n }\n\n throw new AuthError(\n \"No credentials found. Could not authenticate with the Google Play Developer API.\",\n \"AUTH_NO_CREDENTIALS\",\n [\n \"Provide credentials using one of these methods:\",\n \" 1. Pass serviceAccountPath or serviceAccountJson in options\",\n \" 2. Set the GPC_SERVICE_ACCOUNT environment variable to a file path or raw JSON\",\n \" 3. Set GOOGLE_APPLICATION_CREDENTIALS to a service account key file\",\n \" 4. Configure Application Default Credentials: gcloud auth application-default login\",\n ].join(\"\\n\"),\n );\n}\n","export class AuthError extends Error {\n public readonly exitCode = 3;\n constructor(\n message: string,\n public readonly code: string,\n public readonly suggestion?: string,\n ) {\n super(message);\n this.name = \"AuthError\";\n }\n toJSON() {\n return {\n success: false,\n error: {\n code: this.code,\n message: this.message,\n suggestion: this.suggestion,\n },\n };\n }\n}\n","import { readFile } from \"node:fs/promises\";\nimport { resolve } from \"node:path\";\nimport { JWT } from \"google-auth-library\";\nimport { AuthError } from \"./errors.js\";\nimport { acquireToken } from \"./token-cache.js\";\nimport type { AuthClient, ServiceAccountKey } from \"./types.js\";\n\nconst ANDROID_PUBLISHER_SCOPE = \"https://www.googleapis.com/auth/androidpublisher\";\n\nconst REQUIRED_FIELDS: readonly (keyof ServiceAccountKey)[] = [\n \"type\",\n \"private_key\",\n \"client_email\",\n];\n\nfunction validateServiceAccountKey(data: unknown): asserts data is ServiceAccountKey {\n if (typeof data !== \"object\" || data === null) {\n throw new AuthError(\n \"Service account key must be a JSON object.\",\n \"AUTH_INVALID_KEY\",\n \"Ensure the file contains valid JSON with the required service account fields.\",\n );\n }\n\n const record = data as Record<string, unknown>;\n\n for (const field of REQUIRED_FIELDS) {\n if (typeof record[field] !== \"string\" || record[field] === \"\") {\n throw new AuthError(\n `Service account key is missing required field: \"${field}\".`,\n \"AUTH_INVALID_KEY\",\n `Download a fresh service account key from the Google Cloud Console. The key must include: ${REQUIRED_FIELDS.join(\", \")}.`,\n );\n }\n }\n\n if (record[\"type\"] !== \"service_account\") {\n throw new AuthError(\n `Invalid key type \"${String(record[\"type\"])}\". Expected \"service_account\".`,\n \"AUTH_INVALID_KEY\",\n \"Ensure you are using a service account key, not an OAuth client or API key.\",\n );\n }\n}\n\nexport async function loadServiceAccountKey(pathOrJson: string): Promise<ServiceAccountKey> {\n let raw: string;\n\n const trimmed = pathOrJson.trim();\n\n if (trimmed.startsWith(\"{\")) {\n raw = trimmed;\n } else {\n const absolutePath = resolve(trimmed);\n try {\n raw = await readFile(absolutePath, \"utf-8\");\n } catch (err) {\n const code =\n err instanceof Error && \"code\" in err && err.code === \"ENOENT\"\n ? \"AUTH_FILE_NOT_FOUND\"\n : \"AUTH_INVALID_KEY\";\n\n throw new AuthError(\n `Failed to read service account key file: ${absolutePath}`,\n code,\n code === \"AUTH_FILE_NOT_FOUND\"\n ? `File not found. Check that the path is correct: ${absolutePath}`\n : \"Ensure the file is readable and contains valid JSON.\",\n );\n }\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new AuthError(\n \"Failed to parse service account key as JSON.\",\n \"AUTH_INVALID_KEY\",\n \"Ensure the value is valid JSON. If passing a file path, check that the path points to a JSON file.\",\n );\n }\n\n validateServiceAccountKey(parsed);\n return parsed;\n}\n\nconst TOKEN_EXPIRY_SECONDS = 3600; // Google OAuth2 tokens expire in 1 hour\n\nexport function createServiceAccountAuth(key: ServiceAccountKey, cachePath?: string): AuthClient {\n const jwtClient = new JWT({\n email: key.client_email,\n key: key.private_key,\n scopes: [ANDROID_PUBLISHER_SCOPE],\n });\n\n return {\n async getAccessToken(): Promise<string> {\n try {\n return await acquireToken(key.client_email, cachePath, async () => {\n const { token } = await jwtClient.getAccessToken();\n if (!token) {\n throw new AuthError(\n \"Token response was empty.\",\n \"AUTH_TOKEN_FAILED\",\n \"Verify that the service account key is valid and not expired. Check that the private key has not been revoked.\",\n );\n }\n return { token, expiresInSeconds: TOKEN_EXPIRY_SECONDS };\n });\n } catch (err) {\n if (err instanceof AuthError) throw err;\n const rawMsg = err instanceof Error ? err.message : String(err);\n const safeMsg = rawMsg.length > 150 ? rawMsg.slice(0, 150) + \"...\" : rawMsg;\n throw new AuthError(\n `Failed to obtain access token: ${safeMsg}`,\n \"AUTH_TOKEN_FAILED\",\n \"Verify that the service account key is valid and not expired. Check that the private key has not been revoked.\",\n );\n }\n },\n\n getProjectId(): string | undefined {\n return key.project_id || undefined;\n },\n\n getClientEmail(): string {\n return key.client_email;\n },\n };\n}\n","import { chmod, mkdir, readFile, writeFile, rename, unlink } from \"node:fs/promises\";\nimport { dirname, join, isAbsolute } from \"node:path\";\nimport { AuthError } from \"./errors.js\";\n\nexport interface TokenCacheEntry {\n token: string;\n expiresAt: number;\n}\n\nexport type TokenCache = Record<string, TokenCacheEntry>;\n\nconst CACHE_FILE = \"token-cache.json\";\nconst SAFETY_MARGIN_MS = 5 * 60 * 1000; // 5 minutes\n\n// Email must look like a service account email — no path separators or special chars\nconst SAFE_CACHE_KEY = /^[a-zA-Z0-9._%+@-]+$/;\n\n// In-memory cache layer — avoids filesystem I/O on every token request\nconst memoryCache = new Map<string, TokenCacheEntry>();\n\n// Mutex: one in-flight token refresh per email, deduplicates concurrent callers\nconst inflightRefresh = new Map<string, Promise<string>>();\n\nfunction getCachePath(cacheDir: string): string {\n if (!isAbsolute(cacheDir)) {\n throw new AuthError(\n \"Cache directory must be an absolute path\",\n \"AUTH_CACHE_INVALID\",\n \"Provide an absolute path for the token cache directory (e.g., /home/user/.cache/gpc).\",\n );\n }\n return join(cacheDir, CACHE_FILE);\n}\n\nfunction validateCacheKey(email: string): void {\n if (!SAFE_CACHE_KEY.test(email)) {\n throw new AuthError(\n \"Invalid cache key: must be a valid email address\",\n \"AUTH_CACHE_INVALID\",\n \"The cache key must be a valid service account email address (e.g., my-sa@project.iam.gserviceaccount.com).\",\n );\n }\n}\n\nasync function readCache(cacheDir: string): Promise<TokenCache> {\n try {\n const content = await readFile(getCachePath(cacheDir), \"utf-8\");\n return JSON.parse(content) as TokenCache;\n } catch {\n return {};\n }\n}\n\nasync function writeCache(cacheDir: string, cache: TokenCache): Promise<void> {\n const cachePath = getCachePath(cacheDir);\n const tmpPath = cachePath + \".tmp\";\n\n const cacheParent = dirname(cachePath);\n await mkdir(cacheParent, { recursive: true });\n // Restrict cache directory to owner-only (0o700)\n await chmod(cacheParent, 0o700).catch(() => {});\n await writeFile(tmpPath, JSON.stringify(cache, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n await rename(tmpPath, cachePath);\n}\n\nfunction isEntryValid(entry: TokenCacheEntry): boolean {\n return Date.now() < entry.expiresAt - SAFETY_MARGIN_MS;\n}\n\nexport async function getCachedToken(cacheDir: string, email: string): Promise<string | null> {\n validateCacheKey(email);\n\n // Check in-memory cache first — no I/O\n const memEntry = memoryCache.get(email);\n if (memEntry && isEntryValid(memEntry)) {\n return memEntry.token;\n }\n\n // Fall back to filesystem cache\n const cache = await readCache(cacheDir);\n const entry = cache[email];\n\n if (!entry) return null;\n\n if (!isEntryValid(entry)) {\n return null;\n }\n\n // Populate in-memory cache from disk\n memoryCache.set(email, entry);\n return entry.token;\n}\n\nexport async function setCachedToken(\n cacheDir: string,\n email: string,\n token: string,\n expiresInSeconds: number,\n): Promise<void> {\n validateCacheKey(email);\n const entry: TokenCacheEntry = {\n token,\n expiresAt: Date.now() + expiresInSeconds * 1000,\n };\n\n // Update in-memory cache immediately\n memoryCache.set(email, entry);\n\n // Persist to disk\n const cache = await readCache(cacheDir);\n cache[email] = entry;\n await writeCache(cacheDir, cache);\n}\n\nexport async function clearTokenCache(cacheDir: string, email?: string): Promise<void> {\n if (email) {\n memoryCache.delete(email);\n const cache = await readCache(cacheDir);\n const updated = Object.fromEntries(Object.entries(cache).filter(([key]) => key !== email));\n await writeCache(cacheDir, updated);\n } else {\n memoryCache.clear();\n try {\n await unlink(getCachePath(cacheDir));\n } catch {\n // File doesn't exist — nothing to clear\n }\n }\n}\n\n/**\n * Acquire a token with mutex protection.\n * If another caller is already refreshing for this email, waits for that result\n * instead of starting a duplicate refresh.\n */\nexport async function acquireToken(\n email: string,\n cacheDir: string | undefined,\n refresh: () => Promise<{ token: string; expiresInSeconds: number }>,\n): Promise<string> {\n // Fast path: in-memory hit\n const memEntry = memoryCache.get(email);\n if (memEntry && isEntryValid(memEntry)) {\n return memEntry.token;\n }\n\n // Disk cache check (only if cacheDir provided)\n if (cacheDir) {\n const cached = await getCachedToken(cacheDir, email);\n if (cached) return cached;\n }\n\n // Deduplicate concurrent refreshes for the same email\n const inflight = inflightRefresh.get(email);\n if (inflight) return inflight;\n\n const refreshPromise = (async () => {\n const { token, expiresInSeconds } = await refresh();\n\n // Update both memory and disk caches\n const entry: TokenCacheEntry = {\n token,\n expiresAt: Date.now() + expiresInSeconds * 1000,\n };\n memoryCache.set(email, entry);\n\n if (cacheDir) {\n await setCachedToken(cacheDir, email, token, expiresInSeconds).catch(() => {});\n }\n\n return token;\n })();\n\n inflightRefresh.set(email, refreshPromise);\n\n try {\n return await refreshPromise;\n } finally {\n inflightRefresh.delete(email);\n }\n}\n\n/** Reset in-memory state. Exported for testing only. */\nexport function _resetMemoryCache(): void {\n memoryCache.clear();\n inflightRefresh.clear();\n}\n"],"mappings":"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","names":["readFile","readFile","ANDROID_PUBLISHER_SCOPE"]}
|