-
-
Notifications
You must be signed in to change notification settings - Fork 7
/
credentials.ts
136 lines (118 loc) · 3.95 KB
/
credentials.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
/* SPDX-FileCopyrightText: 2022-present Kriasoft */
/* SPDX-License-Identifier: MIT */
import { importPKCS8, importX509, KeyLike } from "jose";
import { FetchError } from "../core/error.js";
const inFlight = new Map<string, Promise<KeyLike>>();
const cache = new Map<string, { key: KeyLike; expires: number }>();
/**
* Normalizes Google Cloud Platform (GCP) service account credentials.
*/
export function getCredentials(credentials: Credentials | string): Credentials {
return typeof credentials === "string" || credentials instanceof String
? Object.freeze(JSON.parse(credentials as string))
: Object.isFrozen(credentials)
? credentials
: Object.freeze(credentials);
}
/**
* Imports a private key from the provided Google Cloud (GCP)
* service account credentials.
*/
export function getPrivateKey(options: { credentials: Credentials | string }) {
const credentials = getCredentials(options.credentials);
return importPKCS8(credentials.private_key, "RS256");
}
/**
* Imports a public key for the provided Google Cloud (GCP)
* service account credentials.
*
* @throws {FetchError} - If the X.509 certificate could not be fetched.
*/
export async function importPublicKey(options: {
/**
* Public key ID (kid).
*/
keyId: string;
/**
* The X.509 certificate URL.
* @default "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"
*/
certificateURL?: string;
/* eslint-disable-next-line @typescript-eslint/no-explicit-any */
waitUntil?: (promise: Promise<any>) => void;
}) {
const keyId = options.keyId;
const certificateURL = options.certificateURL ?? "https://www.googleapis.com/robot/v1/metadata/x509/securetoken@system.gserviceaccount.com"; // prettier-ignore
const cacheKey = `${certificateURL}?key=${keyId}`;
const value = cache.get(cacheKey);
const now = Date.now();
async function fetchKey() {
// Fetch the public key from Google's servers
const res = await fetch(certificateURL);
if (!res.ok) {
const error = await res
.json<{ error: { message: string } }>()
.then((data) => data.error.message)
.catch(() => undefined);
throw new FetchError(error ?? "Failed to fetch the public key", {
response: res,
});
}
const data = await res.json<Record<string, string>>();
const x509 = data[keyId];
if (!x509) {
throw new FetchError(`Public key "${keyId}" not found.`, {
response: res,
});
}
const key = await importX509(x509, "RS256");
// Resolve the expiration time of the key
const maxAge = res.headers.get("cache-control")?.match(/max-age=(\d+)/)?.[1]; // prettier-ignore
const expires = Date.now() + Number(maxAge ?? "3600") * 1000;
// Update the local cache
cache.set(cacheKey, { key, expires });
inFlight.delete(keyId);
return key;
}
// Attempt to read the key from the local cache
if (value) {
if (value.expires > now + 10_000) {
// If the key is about to expire, start a new request in the background
if (value.expires - now < 600_000) {
const promise = fetchKey();
inFlight.set(cacheKey, promise);
if (options.waitUntil) {
options.waitUntil(promise);
}
}
return value.key;
} else {
cache.delete(cacheKey);
}
}
// Check if there is an in-flight request for the same key ID
let promise = inFlight.get(cacheKey);
// If not, start a new request
if (!promise) {
promise = fetchKey();
inFlight.set(cacheKey, promise);
}
return await promise;
}
/**
* Service account credentials for Google Cloud Platform (GCP).
*
* @see https://cloud.google.com/iam/docs/creating-managing-service-account-keys
*/
export type Credentials = {
type: string;
project_id: string;
private_key_id: string;
private_key: string;
client_id: string;
client_email: string;
auth_uri: string;
token_uri: string;
auth_provider_x509_cert_url: string;
client_x509_cert_url: string;
};