@gpc-cli/auth 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/LICENSE +21 -0
- package/dist/index.d.ts +44 -0
- package/dist/index.js +284 -0
- package/dist/index.js.map +1 -0
- package/package.json +38 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 GPC Contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
interface AuthOptions {
|
|
2
|
+
serviceAccountPath?: string;
|
|
3
|
+
serviceAccountJson?: string;
|
|
4
|
+
cachePath?: string;
|
|
5
|
+
}
|
|
6
|
+
interface AuthClient {
|
|
7
|
+
getAccessToken(): Promise<string>;
|
|
8
|
+
getProjectId(): string | undefined;
|
|
9
|
+
getClientEmail(): string;
|
|
10
|
+
}
|
|
11
|
+
interface ServiceAccountKey {
|
|
12
|
+
type: string;
|
|
13
|
+
project_id: string;
|
|
14
|
+
private_key_id: string;
|
|
15
|
+
private_key: string;
|
|
16
|
+
client_email: string;
|
|
17
|
+
client_id: string;
|
|
18
|
+
auth_uri: string;
|
|
19
|
+
token_uri: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
declare function resolveAuth(options?: AuthOptions): Promise<AuthClient>;
|
|
23
|
+
|
|
24
|
+
declare function loadServiceAccountKey(pathOrJson: string): Promise<ServiceAccountKey>;
|
|
25
|
+
declare function createServiceAccountAuth(key: ServiceAccountKey, cachePath?: string): AuthClient;
|
|
26
|
+
|
|
27
|
+
declare function clearTokenCache(cacheDir: string, email?: string): Promise<void>;
|
|
28
|
+
|
|
29
|
+
declare class AuthError extends Error {
|
|
30
|
+
readonly code: string;
|
|
31
|
+
readonly suggestion?: string | undefined;
|
|
32
|
+
readonly exitCode = 3;
|
|
33
|
+
constructor(message: string, code: string, suggestion?: string | undefined);
|
|
34
|
+
toJSON(): {
|
|
35
|
+
success: boolean;
|
|
36
|
+
error: {
|
|
37
|
+
code: string;
|
|
38
|
+
message: string;
|
|
39
|
+
suggestion: string | undefined;
|
|
40
|
+
};
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export { type AuthClient, AuthError, type AuthOptions, type ServiceAccountKey, clearTokenCache, createServiceAccountAuth, loadServiceAccountKey, resolveAuth };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
// src/resolve.ts
|
|
2
|
+
import { GoogleAuth } from "google-auth-library";
|
|
3
|
+
|
|
4
|
+
// src/errors.ts
|
|
5
|
+
var AuthError = class extends Error {
|
|
6
|
+
constructor(message, code, suggestion) {
|
|
7
|
+
super(message);
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.suggestion = suggestion;
|
|
10
|
+
this.name = "AuthError";
|
|
11
|
+
}
|
|
12
|
+
exitCode = 3;
|
|
13
|
+
toJSON() {
|
|
14
|
+
return {
|
|
15
|
+
success: false,
|
|
16
|
+
error: {
|
|
17
|
+
code: this.code,
|
|
18
|
+
message: this.message,
|
|
19
|
+
suggestion: this.suggestion
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
// src/service-account.ts
|
|
26
|
+
import { readFile as readFile2 } from "fs/promises";
|
|
27
|
+
import { resolve } from "path";
|
|
28
|
+
import { JWT } from "google-auth-library";
|
|
29
|
+
|
|
30
|
+
// src/token-cache.ts
|
|
31
|
+
import { chmod, mkdir, readFile, writeFile, rename, unlink } from "fs/promises";
|
|
32
|
+
import { dirname, join, isAbsolute } from "path";
|
|
33
|
+
var CACHE_FILE = "token-cache.json";
|
|
34
|
+
var SAFETY_MARGIN_MS = 5 * 60 * 1e3;
|
|
35
|
+
var SAFE_CACHE_KEY = /^[a-zA-Z0-9._%+@-]+$/;
|
|
36
|
+
function getCachePath(cacheDir) {
|
|
37
|
+
if (!isAbsolute(cacheDir)) {
|
|
38
|
+
throw new Error("Cache directory must be an absolute path");
|
|
39
|
+
}
|
|
40
|
+
return join(cacheDir, CACHE_FILE);
|
|
41
|
+
}
|
|
42
|
+
function validateCacheKey(email) {
|
|
43
|
+
if (!SAFE_CACHE_KEY.test(email)) {
|
|
44
|
+
throw new Error("Invalid cache key: must be a valid email address");
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
async function readCache(cacheDir) {
|
|
48
|
+
try {
|
|
49
|
+
const content = await readFile(getCachePath(cacheDir), "utf-8");
|
|
50
|
+
return JSON.parse(content);
|
|
51
|
+
} catch {
|
|
52
|
+
return {};
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
async function writeCache(cacheDir, cache) {
|
|
56
|
+
const cachePath = getCachePath(cacheDir);
|
|
57
|
+
const tmpPath = cachePath + ".tmp";
|
|
58
|
+
const cacheParent = dirname(cachePath);
|
|
59
|
+
await mkdir(cacheParent, { recursive: true });
|
|
60
|
+
await chmod(cacheParent, 448).catch(() => {
|
|
61
|
+
});
|
|
62
|
+
await writeFile(tmpPath, JSON.stringify(cache, null, 2) + "\n", {
|
|
63
|
+
encoding: "utf-8",
|
|
64
|
+
mode: 384
|
|
65
|
+
});
|
|
66
|
+
await rename(tmpPath, cachePath);
|
|
67
|
+
}
|
|
68
|
+
async function getCachedToken(cacheDir, email) {
|
|
69
|
+
validateCacheKey(email);
|
|
70
|
+
const cache = await readCache(cacheDir);
|
|
71
|
+
const entry = cache[email];
|
|
72
|
+
if (!entry) return null;
|
|
73
|
+
if (Date.now() >= entry.expiresAt - SAFETY_MARGIN_MS) {
|
|
74
|
+
return null;
|
|
75
|
+
}
|
|
76
|
+
return entry.token;
|
|
77
|
+
}
|
|
78
|
+
async function setCachedToken(cacheDir, email, token, expiresInSeconds) {
|
|
79
|
+
validateCacheKey(email);
|
|
80
|
+
const cache = await readCache(cacheDir);
|
|
81
|
+
cache[email] = {
|
|
82
|
+
token,
|
|
83
|
+
expiresAt: Date.now() + expiresInSeconds * 1e3
|
|
84
|
+
};
|
|
85
|
+
await writeCache(cacheDir, cache);
|
|
86
|
+
}
|
|
87
|
+
async function clearTokenCache(cacheDir, email) {
|
|
88
|
+
if (email) {
|
|
89
|
+
const cache = await readCache(cacheDir);
|
|
90
|
+
delete cache[email];
|
|
91
|
+
await writeCache(cacheDir, cache);
|
|
92
|
+
} else {
|
|
93
|
+
try {
|
|
94
|
+
await unlink(getCachePath(cacheDir));
|
|
95
|
+
} catch {
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
// src/service-account.ts
|
|
101
|
+
var ANDROID_PUBLISHER_SCOPE = "https://www.googleapis.com/auth/androidpublisher";
|
|
102
|
+
var REQUIRED_FIELDS = [
|
|
103
|
+
"type",
|
|
104
|
+
"private_key",
|
|
105
|
+
"client_email"
|
|
106
|
+
];
|
|
107
|
+
function validateServiceAccountKey(data) {
|
|
108
|
+
if (typeof data !== "object" || data === null) {
|
|
109
|
+
throw new AuthError(
|
|
110
|
+
"Service account key must be a JSON object.",
|
|
111
|
+
"AUTH_INVALID_KEY",
|
|
112
|
+
"Ensure the file contains valid JSON with the required service account fields."
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
const record = data;
|
|
116
|
+
for (const field of REQUIRED_FIELDS) {
|
|
117
|
+
if (typeof record[field] !== "string" || record[field] === "") {
|
|
118
|
+
throw new AuthError(
|
|
119
|
+
`Service account key is missing required field: "${field}".`,
|
|
120
|
+
"AUTH_INVALID_KEY",
|
|
121
|
+
`Download a fresh service account key from the Google Cloud Console. The key must include: ${REQUIRED_FIELDS.join(", ")}.`
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
if (record["type"] !== "service_account") {
|
|
126
|
+
throw new AuthError(
|
|
127
|
+
`Invalid key type "${String(record["type"])}". Expected "service_account".`,
|
|
128
|
+
"AUTH_INVALID_KEY",
|
|
129
|
+
"Ensure you are using a service account key, not an OAuth client or API key."
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function loadServiceAccountKey(pathOrJson) {
|
|
134
|
+
let raw;
|
|
135
|
+
const trimmed = pathOrJson.trim();
|
|
136
|
+
if (trimmed.startsWith("{")) {
|
|
137
|
+
raw = trimmed;
|
|
138
|
+
} else {
|
|
139
|
+
const absolutePath = resolve(trimmed);
|
|
140
|
+
try {
|
|
141
|
+
raw = await readFile2(absolutePath, "utf-8");
|
|
142
|
+
} catch (err) {
|
|
143
|
+
const code = err instanceof Error && "code" in err && err.code === "ENOENT" ? "AUTH_FILE_NOT_FOUND" : "AUTH_INVALID_KEY";
|
|
144
|
+
throw new AuthError(
|
|
145
|
+
`Failed to read service account key file: ${absolutePath}`,
|
|
146
|
+
code,
|
|
147
|
+
code === "AUTH_FILE_NOT_FOUND" ? `File not found. Check that the path is correct: ${absolutePath}` : "Ensure the file is readable and contains valid JSON."
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
let parsed;
|
|
152
|
+
try {
|
|
153
|
+
parsed = JSON.parse(raw);
|
|
154
|
+
} catch {
|
|
155
|
+
throw new AuthError(
|
|
156
|
+
"Failed to parse service account key as JSON.",
|
|
157
|
+
"AUTH_INVALID_KEY",
|
|
158
|
+
"Ensure the value is valid JSON. If passing a file path, check that the path points to a JSON file."
|
|
159
|
+
);
|
|
160
|
+
}
|
|
161
|
+
validateServiceAccountKey(parsed);
|
|
162
|
+
return parsed;
|
|
163
|
+
}
|
|
164
|
+
var TOKEN_EXPIRY_SECONDS = 3600;
|
|
165
|
+
function createServiceAccountAuth(key, cachePath) {
|
|
166
|
+
const jwtClient = new JWT({
|
|
167
|
+
email: key.client_email,
|
|
168
|
+
key: key.private_key,
|
|
169
|
+
scopes: [ANDROID_PUBLISHER_SCOPE]
|
|
170
|
+
});
|
|
171
|
+
return {
|
|
172
|
+
async getAccessToken() {
|
|
173
|
+
if (cachePath) {
|
|
174
|
+
const cached = await getCachedToken(cachePath, key.client_email);
|
|
175
|
+
if (cached) return cached;
|
|
176
|
+
}
|
|
177
|
+
try {
|
|
178
|
+
const { token } = await jwtClient.getAccessToken();
|
|
179
|
+
if (!token) {
|
|
180
|
+
throw new Error("Token response was empty.");
|
|
181
|
+
}
|
|
182
|
+
if (cachePath) {
|
|
183
|
+
await setCachedToken(cachePath, key.client_email, token, TOKEN_EXPIRY_SECONDS).catch(() => {
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
return token;
|
|
187
|
+
} catch (err) {
|
|
188
|
+
const rawMsg = err instanceof Error ? err.message : String(err);
|
|
189
|
+
const safeMsg = rawMsg.length > 150 ? rawMsg.slice(0, 150) + "..." : rawMsg;
|
|
190
|
+
throw new AuthError(
|
|
191
|
+
`Failed to obtain access token: ${safeMsg}`,
|
|
192
|
+
"AUTH_TOKEN_FAILED",
|
|
193
|
+
"Verify that the service account key is valid and not expired. Check that the private key has not been revoked."
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
},
|
|
197
|
+
getProjectId() {
|
|
198
|
+
return key.project_id || void 0;
|
|
199
|
+
},
|
|
200
|
+
getClientEmail() {
|
|
201
|
+
return key.client_email;
|
|
202
|
+
}
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
// src/resolve.ts
|
|
207
|
+
var ANDROID_PUBLISHER_SCOPE2 = "https://www.googleapis.com/auth/androidpublisher";
|
|
208
|
+
async function tryApplicationDefaultCredentials() {
|
|
209
|
+
try {
|
|
210
|
+
const auth = new GoogleAuth({
|
|
211
|
+
scopes: [ANDROID_PUBLISHER_SCOPE2]
|
|
212
|
+
});
|
|
213
|
+
const client = await auth.getClient();
|
|
214
|
+
const projectId = await auth.getProjectId().catch(() => void 0);
|
|
215
|
+
const email = client.email;
|
|
216
|
+
return {
|
|
217
|
+
async getAccessToken() {
|
|
218
|
+
const { token } = await client.getAccessToken();
|
|
219
|
+
if (!token) {
|
|
220
|
+
throw new AuthError(
|
|
221
|
+
"Application Default Credentials returned an empty token.",
|
|
222
|
+
"AUTH_TOKEN_FAILED",
|
|
223
|
+
"Verify your ADC configuration with: gcloud auth application-default print-access-token"
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
return token;
|
|
227
|
+
},
|
|
228
|
+
getProjectId() {
|
|
229
|
+
return projectId ?? void 0;
|
|
230
|
+
},
|
|
231
|
+
getClientEmail() {
|
|
232
|
+
return email ?? "unknown";
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
} catch {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
async function resolveAuth(options) {
|
|
240
|
+
if (options?.serviceAccountJson) {
|
|
241
|
+
const key = await loadServiceAccountKey(options.serviceAccountJson);
|
|
242
|
+
return createServiceAccountAuth(key, options?.cachePath);
|
|
243
|
+
}
|
|
244
|
+
if (options?.serviceAccountPath) {
|
|
245
|
+
const key = await loadServiceAccountKey(options.serviceAccountPath);
|
|
246
|
+
return createServiceAccountAuth(key, options?.cachePath);
|
|
247
|
+
}
|
|
248
|
+
const envValue = process.env["GPC_SERVICE_ACCOUNT"];
|
|
249
|
+
if (envValue) {
|
|
250
|
+
const key = await loadServiceAccountKey(envValue);
|
|
251
|
+
return createServiceAccountAuth(key, options?.cachePath);
|
|
252
|
+
}
|
|
253
|
+
const gacPath = process.env["GOOGLE_APPLICATION_CREDENTIALS"];
|
|
254
|
+
if (gacPath) {
|
|
255
|
+
try {
|
|
256
|
+
const key = await loadServiceAccountKey(gacPath);
|
|
257
|
+
return createServiceAccountAuth(key, options?.cachePath);
|
|
258
|
+
} catch {
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
const adcClient = await tryApplicationDefaultCredentials();
|
|
262
|
+
if (adcClient) {
|
|
263
|
+
return adcClient;
|
|
264
|
+
}
|
|
265
|
+
throw new AuthError(
|
|
266
|
+
"No credentials found. Could not authenticate with the Google Play Developer API.",
|
|
267
|
+
"AUTH_NO_CREDENTIALS",
|
|
268
|
+
[
|
|
269
|
+
"Provide credentials using one of these methods:",
|
|
270
|
+
" 1. Pass serviceAccountPath or serviceAccountJson in options",
|
|
271
|
+
" 2. Set the GPC_SERVICE_ACCOUNT environment variable to a file path or raw JSON",
|
|
272
|
+
" 3. Set GOOGLE_APPLICATION_CREDENTIALS to a service account key file",
|
|
273
|
+
" 4. Configure Application Default Credentials: gcloud auth application-default login"
|
|
274
|
+
].join("\n")
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
export {
|
|
278
|
+
AuthError,
|
|
279
|
+
clearTokenCache,
|
|
280
|
+
createServiceAccountAuth,
|
|
281
|
+
loadServiceAccountKey,
|
|
282
|
+
resolveAuth
|
|
283
|
+
};
|
|
284
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +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 type { AuthClient, AuthOptions } from \"./types.js\";\n\nconst ANDROID_PUBLISHER_SCOPE =\n \"https://www.googleapis.com/auth/androidpublisher\";\n\nasync function tryApplicationDefaultCredentials(): 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;\n\n return {\n async getAccessToken(): Promise<string> {\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;\n },\n\n getProjectId(): string | undefined {\n return projectId ?? undefined;\n },\n\n getClientEmail(): string {\n return email ?? \"unknown\";\n },\n };\n } catch {\n return null;\n }\n}\n\nexport async function resolveAuth(\n options?: AuthOptions,\n): 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();\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 { getCachedToken, setCachedToken } from \"./token-cache.js\";\nimport type { AuthClient, ServiceAccountKey } from \"./types.js\";\n\nconst ANDROID_PUBLISHER_SCOPE =\n \"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(\n data: unknown,\n): 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(\n pathOrJson: string,\n): 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 // Check cache first\n if (cachePath) {\n const cached = await getCachedToken(cachePath, key.client_email);\n if (cached) return cached;\n }\n\n try {\n const { token } = await jwtClient.getAccessToken();\n if (!token) {\n throw new Error(\"Token response was empty.\");\n }\n\n // Cache the token\n if (cachePath) {\n await setCachedToken(cachePath, key.client_email, token, TOKEN_EXPIRY_SECONDS).catch(() => {});\n }\n\n return token;\n } catch (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\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\nexport async function getCachedToken(\n cacheDir: string,\n email: string,\n): Promise<string | null> {\n validateCacheKey(email);\n const cache = await readCache(cacheDir);\n const entry = cache[email];\n\n if (!entry) return null;\n\n // Check expiry with safety margin\n if (Date.now() >= entry.expiresAt - SAFETY_MARGIN_MS) {\n return null;\n }\n\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 cache = await readCache(cacheDir);\n cache[email] = {\n token,\n expiresAt: Date.now() + expiresInSeconds * 1000,\n };\n await writeCache(cacheDir, cache);\n}\n\nexport async function clearTokenCache(\n cacheDir: string,\n email?: string,\n): Promise<void> {\n if (email) {\n const cache = await readCache(cacheDir);\n delete cache[email];\n await writeCache(cacheDir, cache);\n } else {\n try {\n await unlink(getCachePath(cacheDir));\n } catch {\n // File doesn't exist — nothing to clear\n }\n }\n}\n"],"mappings":"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","names":["readFile","readFile","ANDROID_PUBLISHER_SCOPE"]}
|
package/package.json
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@gpc-cli/auth",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Authentication strategies for Google Play Developer API",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"import": "./dist/index.js",
|
|
11
|
+
"types": "./dist/index.d.ts"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"keywords": [
|
|
18
|
+
"google-play",
|
|
19
|
+
"auth",
|
|
20
|
+
"service-account"
|
|
21
|
+
],
|
|
22
|
+
"license": "MIT",
|
|
23
|
+
"devDependencies": {
|
|
24
|
+
"@types/node": "^25.3.5"
|
|
25
|
+
},
|
|
26
|
+
"dependencies": {
|
|
27
|
+
"google-auth-library": "^10.6.1"
|
|
28
|
+
},
|
|
29
|
+
"scripts": {
|
|
30
|
+
"build": "tsup",
|
|
31
|
+
"dev": "tsup --watch",
|
|
32
|
+
"test": "vitest run",
|
|
33
|
+
"test:watch": "vitest",
|
|
34
|
+
"lint": "eslint src/",
|
|
35
|
+
"typecheck": "tsc --noEmit",
|
|
36
|
+
"clean": "rm -rf dist"
|
|
37
|
+
}
|
|
38
|
+
}
|