@pymthouse/builder-sdk 0.4.4 → 0.4.5
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 +120 -5
- package/dist/{client-zCskUJag.d.ts → client-BhNz0ZAA.d.ts} +9 -3
- package/dist/{client-C0HgAugK.d.cts → client-GP-mTEI7.d.cts} +9 -3
- package/dist/device.d.cts +1 -1
- package/dist/device.d.ts +1 -1
- package/dist/env.cjs +40 -3
- package/dist/env.cjs.map +1 -1
- package/dist/env.d.cts +2 -2
- package/dist/env.d.ts +2 -2
- package/dist/env.js +40 -3
- package/dist/env.js.map +1 -1
- package/dist/{index-CAIAYJv7.d.cts → index-M0tsyotJ.d.cts} +1 -1
- package/dist/{index-BL1wpOki.d.ts → index-rC8smShg.d.ts} +1 -1
- package/dist/index.cjs +40 -3
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +4 -4
- package/dist/index.d.ts +4 -4
- package/dist/index.js +40 -3
- package/dist/index.js.map +1 -1
- package/dist/{proxy-KrA1vEmh.d.ts → proxy-CZLY0IfL.d.cts} +5 -2
- package/dist/{proxy-0wa8QZIU.d.cts → proxy-D36SpZ6k.d.ts} +5 -2
- package/dist/signer/gateway.cjs +542 -0
- package/dist/signer/gateway.cjs.map +1 -0
- package/dist/signer/gateway.d.cts +81 -0
- package/dist/signer/gateway.d.ts +81 -0
- package/dist/signer/gateway.js +538 -0
- package/dist/signer/gateway.js.map +1 -0
- package/dist/signer/server.cjs +225 -0
- package/dist/signer/server.cjs.map +1 -1
- package/dist/signer/server.d.cts +35 -4
- package/dist/signer/server.d.ts +35 -4
- package/dist/signer/server.js +219 -1
- package/dist/signer/server.js.map +1 -1
- package/dist/signer/webhook/adapters/oidc.d.cts +2 -2
- package/dist/signer/webhook/adapters/oidc.d.ts +2 -2
- package/dist/signer/webhook.d.cts +3 -3
- package/dist/signer/webhook.d.ts +3 -3
- package/dist/tokens.d.cts +1 -1
- package/dist/tokens.d.ts +1 -1
- package/dist/{types-BORaHW_x.d.cts → types-CcP67AZm.d.cts} +2 -0
- package/dist/{types-BORaHW_x.d.ts → types-CcP67AZm.d.ts} +2 -0
- package/dist/verify.d.cts +1 -1
- package/dist/verify.d.ts +1 -1
- package/package.json +6 -1
|
@@ -0,0 +1,538 @@
|
|
|
1
|
+
import { discoveryRequest, processDiscoveryResponse, customFetch, allowInsecureRequests } from 'oauth4webapi';
|
|
2
|
+
|
|
3
|
+
// src/errors.ts
|
|
4
|
+
var PmtHouseError = class extends Error {
|
|
5
|
+
status;
|
|
6
|
+
code;
|
|
7
|
+
details;
|
|
8
|
+
constructor(message, {
|
|
9
|
+
status = 500,
|
|
10
|
+
code = "pymthouse_error",
|
|
11
|
+
details
|
|
12
|
+
} = {}) {
|
|
13
|
+
super(message);
|
|
14
|
+
this.name = "PmtHouseError";
|
|
15
|
+
this.status = status;
|
|
16
|
+
this.code = code;
|
|
17
|
+
this.details = details;
|
|
18
|
+
}
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
// src/string-utils.ts
|
|
22
|
+
function stripTrailingSlashes(value) {
|
|
23
|
+
let end = value.length;
|
|
24
|
+
while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
|
|
25
|
+
end--;
|
|
26
|
+
}
|
|
27
|
+
return value.slice(0, end);
|
|
28
|
+
}
|
|
29
|
+
function endsWithIgnoreCase(value, suffix) {
|
|
30
|
+
if (suffix.length > value.length) {
|
|
31
|
+
return false;
|
|
32
|
+
}
|
|
33
|
+
const start = value.length - suffix.length;
|
|
34
|
+
for (let i = 0; i < suffix.length; i++) {
|
|
35
|
+
const a = value.codePointAt(start + i) ?? 0;
|
|
36
|
+
const b = suffix.codePointAt(i) ?? 0;
|
|
37
|
+
if (a !== b && (a | 32) !== (b | 32)) {
|
|
38
|
+
return false;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
function stripSuffixIgnoreCase(value, suffix) {
|
|
44
|
+
return endsWithIgnoreCase(value, suffix) ? value.slice(0, value.length - suffix.length) : value;
|
|
45
|
+
}
|
|
46
|
+
function stripIssuerOriginFromOidcUrl(issuerUrl) {
|
|
47
|
+
let base = stripTrailingSlashes(issuerUrl.trim());
|
|
48
|
+
base = stripSuffixIgnoreCase(base, "/api/v1/oidc");
|
|
49
|
+
base = stripSuffixIgnoreCase(base, "/oidc");
|
|
50
|
+
return stripTrailingSlashes(base);
|
|
51
|
+
}
|
|
52
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
53
|
+
var discoveryCache = /* @__PURE__ */ new Map();
|
|
54
|
+
function normalizedIssuerKey(issuerUrl) {
|
|
55
|
+
return stripTrailingSlashes(issuerUrl);
|
|
56
|
+
}
|
|
57
|
+
async function loadAuthorizationServer(issuerUrl, fetchImpl, options = {}) {
|
|
58
|
+
const key = normalizedIssuerKey(issuerUrl);
|
|
59
|
+
const now = Date.now();
|
|
60
|
+
const cached = discoveryCache.get(key);
|
|
61
|
+
if (!options.force && cached && now - cached.fetchedAt < CACHE_TTL_MS) {
|
|
62
|
+
return cached.as;
|
|
63
|
+
}
|
|
64
|
+
const issuerIdentifier = new URL(key);
|
|
65
|
+
const discoveryOpts = {
|
|
66
|
+
algorithm: "oidc",
|
|
67
|
+
[customFetch]: fetchImpl
|
|
68
|
+
};
|
|
69
|
+
if (options.allowInsecureHttp) {
|
|
70
|
+
discoveryOpts[allowInsecureRequests] = true;
|
|
71
|
+
}
|
|
72
|
+
let response;
|
|
73
|
+
try {
|
|
74
|
+
response = await discoveryRequest(issuerIdentifier, discoveryOpts);
|
|
75
|
+
} catch (e) {
|
|
76
|
+
throw mapDiscoveryNetworkError(e);
|
|
77
|
+
}
|
|
78
|
+
let as;
|
|
79
|
+
try {
|
|
80
|
+
as = await processDiscoveryResponse(issuerIdentifier, response);
|
|
81
|
+
} catch (e) {
|
|
82
|
+
throw mapOAuthDiscoveryError(e);
|
|
83
|
+
}
|
|
84
|
+
discoveryCache.set(key, { as, fetchedAt: now });
|
|
85
|
+
return as;
|
|
86
|
+
}
|
|
87
|
+
function mapOAuthDiscoveryError(error) {
|
|
88
|
+
if (error instanceof PmtHouseError) {
|
|
89
|
+
return error;
|
|
90
|
+
}
|
|
91
|
+
if (error instanceof Error) {
|
|
92
|
+
return new PmtHouseError(error.message, {
|
|
93
|
+
status: 500,
|
|
94
|
+
code: "oidc_discovery_invalid",
|
|
95
|
+
details: { cause: error.cause }
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
return new PmtHouseError("OIDC discovery failed", {
|
|
99
|
+
status: 500,
|
|
100
|
+
code: "oidc_discovery_invalid"
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function mapDiscoveryNetworkError(error) {
|
|
104
|
+
if (error instanceof PmtHouseError) {
|
|
105
|
+
return error;
|
|
106
|
+
}
|
|
107
|
+
if (error instanceof Error) {
|
|
108
|
+
return new PmtHouseError(`Failed to load OIDC discovery: ${error.message}`, {
|
|
109
|
+
status: 502,
|
|
110
|
+
code: "oidc_discovery_failed"
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
return new PmtHouseError("Failed to load OIDC discovery", {
|
|
114
|
+
status: 502,
|
|
115
|
+
code: "oidc_discovery_failed"
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// src/encoding.ts
|
|
120
|
+
function encodeClientSecretBasic(clientId, clientSecret) {
|
|
121
|
+
const raw = `${clientId}:${clientSecret}`;
|
|
122
|
+
const b64 = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(Array.from(new TextEncoder().encode(raw), (c) => String.fromCharCode(c)).join(""));
|
|
123
|
+
return `Basic ${b64}`;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
// src/signer/fetch-json.ts
|
|
127
|
+
function oauthFailureDescription(parsed, failureLabel, status) {
|
|
128
|
+
if (typeof parsed.error_description === "string") {
|
|
129
|
+
return parsed.error_description;
|
|
130
|
+
}
|
|
131
|
+
if (typeof parsed.error === "string") {
|
|
132
|
+
return parsed.error;
|
|
133
|
+
}
|
|
134
|
+
return `${failureLabel} (${status})`;
|
|
135
|
+
}
|
|
136
|
+
async function readJsonObjectFromResponse(response, options) {
|
|
137
|
+
const text = await response.text();
|
|
138
|
+
let parsed;
|
|
139
|
+
try {
|
|
140
|
+
parsed = text ? JSON.parse(text) : {};
|
|
141
|
+
} catch {
|
|
142
|
+
throw new PmtHouseError(options.invalidJsonMessage, {
|
|
143
|
+
status: 502,
|
|
144
|
+
code: options.invalidJsonCode,
|
|
145
|
+
details: { status: response.status }
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
if (!response.ok) {
|
|
149
|
+
const description = oauthFailureDescription(parsed, options.failureLabel, response.status);
|
|
150
|
+
throw new PmtHouseError(description, {
|
|
151
|
+
status: response.status,
|
|
152
|
+
code: typeof parsed.error === "string" ? parsed.error : options.defaultErrorCode,
|
|
153
|
+
details: parsed
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
return parsed;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// src/signer/json-fields.ts
|
|
160
|
+
function readStringField(body, key, errorCode, messagePrefix = "Response") {
|
|
161
|
+
const value = body[key];
|
|
162
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
163
|
+
throw new PmtHouseError(`${messagePrefix} missing ${key}`, {
|
|
164
|
+
status: 502,
|
|
165
|
+
code: errorCode
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return value.trim();
|
|
169
|
+
}
|
|
170
|
+
function readExpiresIn(body, errorCode) {
|
|
171
|
+
const expiresIn = body.expires_in;
|
|
172
|
+
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
173
|
+
throw new PmtHouseError("Response missing expires_in", {
|
|
174
|
+
status: 502,
|
|
175
|
+
code: errorCode
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
return Math.floor(expiresIn);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
// src/signer/mint-token.ts
|
|
182
|
+
var SIGN_MINT_USER_TOKEN_SCOPE = "sign:mint_user_token";
|
|
183
|
+
var DEFAULT_TTL_REFRESH_RATIO = 0.8;
|
|
184
|
+
var TOKEN_RESPONSE_ERROR = "invalid_token_response";
|
|
185
|
+
function signerJwtAudience(issuerUrl) {
|
|
186
|
+
return stripTrailingSlashes(issuerUrl);
|
|
187
|
+
}
|
|
188
|
+
function parseMintUserSignerTokenResponse(body, ttlRefreshRatio = DEFAULT_TTL_REFRESH_RATIO) {
|
|
189
|
+
const accessToken = readStringField(body, "access_token", TOKEN_RESPONSE_ERROR, "Token response");
|
|
190
|
+
const expiresIn = readExpiresIn(body, TOKEN_RESPONSE_ERROR);
|
|
191
|
+
const balanceUsdMicros = readStringField(
|
|
192
|
+
body,
|
|
193
|
+
"balanceUsdMicros",
|
|
194
|
+
TOKEN_RESPONSE_ERROR,
|
|
195
|
+
"Token response"
|
|
196
|
+
);
|
|
197
|
+
const lifetimeGrantedUsdMicros = readStringField(
|
|
198
|
+
body,
|
|
199
|
+
"lifetimeGrantedUsdMicros",
|
|
200
|
+
TOKEN_RESPONSE_ERROR,
|
|
201
|
+
"Token response"
|
|
202
|
+
);
|
|
203
|
+
const now = Date.now();
|
|
204
|
+
const expiresAt = now + expiresIn * 1e3;
|
|
205
|
+
const refreshAt = now + Math.floor(expiresIn * 1e3 * ttlRefreshRatio);
|
|
206
|
+
return {
|
|
207
|
+
jwt: accessToken,
|
|
208
|
+
expiresAt,
|
|
209
|
+
refreshAt,
|
|
210
|
+
balanceUsdMicros,
|
|
211
|
+
lifetimeGrantedUsdMicros
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
async function mintUserSignerToken(options) {
|
|
215
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
216
|
+
const issuerUrl = stripTrailingSlashes(options.issuerUrl);
|
|
217
|
+
const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {
|
|
218
|
+
allowInsecureHttp: options.allowInsecureHttp
|
|
219
|
+
});
|
|
220
|
+
const tokenEndpoint = as.token_endpoint;
|
|
221
|
+
if (!tokenEndpoint) {
|
|
222
|
+
throw new PmtHouseError("OIDC discovery document is missing token_endpoint", {
|
|
223
|
+
status: 500,
|
|
224
|
+
code: "oidc_discovery_invalid"
|
|
225
|
+
});
|
|
226
|
+
}
|
|
227
|
+
const audience = signerJwtAudience(issuerUrl);
|
|
228
|
+
const body = new URLSearchParams({
|
|
229
|
+
grant_type: "client_credentials",
|
|
230
|
+
scope: SIGN_MINT_USER_TOKEN_SCOPE,
|
|
231
|
+
external_user_id: options.externalUserId,
|
|
232
|
+
audience
|
|
233
|
+
});
|
|
234
|
+
const response = await fetchImpl(tokenEndpoint, {
|
|
235
|
+
method: "POST",
|
|
236
|
+
headers: {
|
|
237
|
+
Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),
|
|
238
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
239
|
+
Accept: "application/json"
|
|
240
|
+
},
|
|
241
|
+
body: body.toString(),
|
|
242
|
+
cache: "no-store"
|
|
243
|
+
});
|
|
244
|
+
const parsed = await readJsonObjectFromResponse(response, {
|
|
245
|
+
invalidJsonMessage: "Token endpoint returned invalid JSON",
|
|
246
|
+
invalidJsonCode: TOKEN_RESPONSE_ERROR,
|
|
247
|
+
failureLabel: "Token mint failed",
|
|
248
|
+
defaultErrorCode: "token_mint_failed"
|
|
249
|
+
});
|
|
250
|
+
return parseMintUserSignerTokenResponse(parsed);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// src/signer/device-exchange.ts
|
|
254
|
+
var TOKEN_EXCHANGE_GRANT = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
255
|
+
var SUBJECT_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
|
|
256
|
+
var EXCHANGE_RESPONSE_ERROR = "invalid_exchange_response";
|
|
257
|
+
async function mintSignerTokenFromDeviceToken(options) {
|
|
258
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
259
|
+
const issuerUrl = stripTrailingSlashes(options.issuerUrl);
|
|
260
|
+
const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {
|
|
261
|
+
allowInsecureHttp: options.allowInsecureHttp
|
|
262
|
+
});
|
|
263
|
+
const tokenEndpoint = as.token_endpoint;
|
|
264
|
+
if (!tokenEndpoint) {
|
|
265
|
+
throw new PmtHouseError("OIDC discovery document is missing token_endpoint", {
|
|
266
|
+
status: 500,
|
|
267
|
+
code: "oidc_discovery_invalid"
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
const audience = options.audience?.trim() || issuerUrl;
|
|
271
|
+
const params = new URLSearchParams({
|
|
272
|
+
grant_type: TOKEN_EXCHANGE_GRANT,
|
|
273
|
+
subject_token: options.deviceToken,
|
|
274
|
+
subject_token_type: SUBJECT_ACCESS_TOKEN_TYPE,
|
|
275
|
+
audience,
|
|
276
|
+
resource: audience
|
|
277
|
+
});
|
|
278
|
+
if (options.scope?.trim()) {
|
|
279
|
+
params.set("scope", options.scope.trim());
|
|
280
|
+
}
|
|
281
|
+
const response = await fetchImpl(tokenEndpoint, {
|
|
282
|
+
method: "POST",
|
|
283
|
+
headers: {
|
|
284
|
+
Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),
|
|
285
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
286
|
+
Accept: "application/json"
|
|
287
|
+
},
|
|
288
|
+
body: params.toString(),
|
|
289
|
+
cache: "no-store"
|
|
290
|
+
});
|
|
291
|
+
const parsed = await readJsonObjectFromResponse(response, {
|
|
292
|
+
invalidJsonMessage: "Token endpoint returned invalid JSON",
|
|
293
|
+
invalidJsonCode: "invalid_token_response",
|
|
294
|
+
failureLabel: "Signer JWT exchange failed",
|
|
295
|
+
defaultErrorCode: "token_exchange_failed"
|
|
296
|
+
});
|
|
297
|
+
const cached = parseMintUserSignerTokenResponse(parsed);
|
|
298
|
+
return {
|
|
299
|
+
access_token: cached.jwt,
|
|
300
|
+
expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),
|
|
301
|
+
scope: readStringField(parsed, "scope", EXCHANGE_RESPONSE_ERROR),
|
|
302
|
+
balanceUsdMicros: cached.balanceUsdMicros,
|
|
303
|
+
lifetimeGrantedUsdMicros: cached.lifetimeGrantedUsdMicros
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
// src/signer/api-key-exchange.ts
|
|
308
|
+
var EXCHANGE_RESPONSE_ERROR2 = "invalid_exchange_response";
|
|
309
|
+
async function mintUserAccessTokenFromApiKey(input) {
|
|
310
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
311
|
+
const issuerOrigin = stripIssuerOriginFromOidcUrl(input.issuerUrl);
|
|
312
|
+
const url = `${issuerOrigin}/api/v1/apps/${encodeURIComponent(input.publicClientId)}/auth/api-key/token`;
|
|
313
|
+
const response = await fetchImpl(url, {
|
|
314
|
+
method: "POST",
|
|
315
|
+
headers: {
|
|
316
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
317
|
+
"Content-Type": "application/json",
|
|
318
|
+
Accept: "application/json"
|
|
319
|
+
},
|
|
320
|
+
body: JSON.stringify(input.scope ? { scope: input.scope } : {}),
|
|
321
|
+
cache: "no-store"
|
|
322
|
+
});
|
|
323
|
+
const parsed = await readJsonObjectFromResponse(response, {
|
|
324
|
+
invalidJsonMessage: "API key token exchange returned invalid JSON",
|
|
325
|
+
invalidJsonCode: "invalid_token_response",
|
|
326
|
+
failureLabel: "API key token exchange failed",
|
|
327
|
+
defaultErrorCode: "api_key_token_exchange_failed"
|
|
328
|
+
});
|
|
329
|
+
const accessToken = parsed.access_token;
|
|
330
|
+
if (typeof accessToken !== "string" || !accessToken.trim()) {
|
|
331
|
+
throw new PmtHouseError("API key token exchange missing access_token", {
|
|
332
|
+
status: 502,
|
|
333
|
+
code: EXCHANGE_RESPONSE_ERROR2
|
|
334
|
+
});
|
|
335
|
+
}
|
|
336
|
+
const expiresIn = typeof parsed.expires_in === "number" && Number.isFinite(parsed.expires_in) ? parsed.expires_in : 900;
|
|
337
|
+
const scope = typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : input.scope?.trim() || "sign:job";
|
|
338
|
+
return {
|
|
339
|
+
access_token: accessToken.trim(),
|
|
340
|
+
expires_in: expiresIn,
|
|
341
|
+
scope
|
|
342
|
+
};
|
|
343
|
+
}
|
|
344
|
+
async function mintSignerSessionFromApiKey(input) {
|
|
345
|
+
const userToken = await mintUserAccessTokenFromApiKey({
|
|
346
|
+
issuerUrl: input.issuerUrl,
|
|
347
|
+
publicClientId: input.publicClientId,
|
|
348
|
+
apiKey: input.apiKey,
|
|
349
|
+
scope: input.scope,
|
|
350
|
+
fetch: input.fetch
|
|
351
|
+
});
|
|
352
|
+
return mintSignerTokenFromDeviceToken({
|
|
353
|
+
issuerUrl: input.issuerUrl,
|
|
354
|
+
m2mClientId: input.m2mClientId,
|
|
355
|
+
m2mClientSecret: input.m2mClientSecret,
|
|
356
|
+
deviceToken: userToken.access_token,
|
|
357
|
+
scope: userToken.scope,
|
|
358
|
+
audience: input.audience,
|
|
359
|
+
fetch: input.fetch,
|
|
360
|
+
allowInsecureHttp: input.allowInsecureHttp
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// src/signer/gateway-token.ts
|
|
365
|
+
function requireString(value, label) {
|
|
366
|
+
if (typeof value !== "string") {
|
|
367
|
+
throw new PmtHouseError(`${label} must be a string`, {
|
|
368
|
+
status: 400,
|
|
369
|
+
code: "invalid_gateway_token"
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return value.trim();
|
|
373
|
+
}
|
|
374
|
+
function optionalString(value, label) {
|
|
375
|
+
if (value === void 0 || value === null) {
|
|
376
|
+
return void 0;
|
|
377
|
+
}
|
|
378
|
+
return requireString(value, label) || void 0;
|
|
379
|
+
}
|
|
380
|
+
function encodeBase64Json(value) {
|
|
381
|
+
const json = JSON.stringify(value);
|
|
382
|
+
if (typeof Buffer === "undefined") {
|
|
383
|
+
const binary = Array.from(
|
|
384
|
+
new TextEncoder().encode(json),
|
|
385
|
+
(c) => String.fromCodePoint(c)
|
|
386
|
+
).join("");
|
|
387
|
+
return btoa(binary);
|
|
388
|
+
}
|
|
389
|
+
return Buffer.from(json, "utf8").toString("base64");
|
|
390
|
+
}
|
|
391
|
+
function decodeBase64Json(token) {
|
|
392
|
+
const trimmed = requireString(token, "gateway token");
|
|
393
|
+
let json;
|
|
394
|
+
try {
|
|
395
|
+
if (typeof Buffer === "undefined") {
|
|
396
|
+
json = new TextDecoder().decode(
|
|
397
|
+
Uint8Array.from(atob(trimmed), (c) => c.codePointAt(0) ?? 0)
|
|
398
|
+
);
|
|
399
|
+
} else {
|
|
400
|
+
json = Buffer.from(trimmed, "base64").toString("utf8");
|
|
401
|
+
}
|
|
402
|
+
} catch {
|
|
403
|
+
throw new PmtHouseError("Invalid gateway token: expected base64-encoded JSON", {
|
|
404
|
+
status: 400,
|
|
405
|
+
code: "invalid_gateway_token"
|
|
406
|
+
});
|
|
407
|
+
}
|
|
408
|
+
try {
|
|
409
|
+
return JSON.parse(json);
|
|
410
|
+
} catch {
|
|
411
|
+
throw new PmtHouseError("Invalid gateway token: expected UTF-8 JSON payload", {
|
|
412
|
+
status: 400,
|
|
413
|
+
code: "invalid_gateway_token"
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
function normalizeStringMap(map) {
|
|
418
|
+
if (!map) {
|
|
419
|
+
return void 0;
|
|
420
|
+
}
|
|
421
|
+
const entries = Object.entries(map).filter(
|
|
422
|
+
([key, value]) => key.trim() && typeof value === "string"
|
|
423
|
+
);
|
|
424
|
+
return entries.length > 0 ? Object.fromEntries(entries) : void 0;
|
|
425
|
+
}
|
|
426
|
+
function buildGatewayToken(input) {
|
|
427
|
+
if (input === null || typeof input !== "object") {
|
|
428
|
+
throw new PmtHouseError("buildGatewayToken requires an input object", {
|
|
429
|
+
status: 400,
|
|
430
|
+
code: "invalid_gateway_token"
|
|
431
|
+
});
|
|
432
|
+
}
|
|
433
|
+
const signer = requireString(input.signer, "signer URL");
|
|
434
|
+
if (!signer) {
|
|
435
|
+
throw new PmtHouseError("buildGatewayToken requires a non-empty signer URL", {
|
|
436
|
+
status: 400,
|
|
437
|
+
code: "invalid_gateway_token"
|
|
438
|
+
});
|
|
439
|
+
}
|
|
440
|
+
const signerHeaders = { ...input.signerHeaders };
|
|
441
|
+
const bundle = { signer };
|
|
442
|
+
const discovery = optionalString(input.discovery, "discovery URL");
|
|
443
|
+
if (discovery) {
|
|
444
|
+
bundle.discovery = discovery;
|
|
445
|
+
}
|
|
446
|
+
const rawOrchestrators = input.orchestrators ?? [];
|
|
447
|
+
if (!Array.isArray(rawOrchestrators)) {
|
|
448
|
+
throw new PmtHouseError("orchestrators must be an array of strings", {
|
|
449
|
+
status: 400,
|
|
450
|
+
code: "invalid_gateway_token"
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
const orchestrators = rawOrchestrators.map((entry) => requireString(entry, "orchestrator entry")).filter((entry) => entry.length > 0);
|
|
454
|
+
if (orchestrators.length > 0) {
|
|
455
|
+
bundle.orchestrators = orchestrators;
|
|
456
|
+
}
|
|
457
|
+
if (input.auth?.kind === "signerJwt") {
|
|
458
|
+
const accessToken = requireString(input.auth.accessToken, "signerJwt accessToken");
|
|
459
|
+
if (!accessToken) {
|
|
460
|
+
throw new PmtHouseError("signerJwt auth requires a non-empty accessToken", {
|
|
461
|
+
status: 400,
|
|
462
|
+
code: "invalid_gateway_token"
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
signerHeaders.Authorization = `Bearer ${accessToken}`;
|
|
466
|
+
} else if (input.auth?.kind === "pmthApiKey") {
|
|
467
|
+
const apiKey = requireString(input.auth.apiKey, "pmthApiKey apiKey");
|
|
468
|
+
if (!apiKey) {
|
|
469
|
+
throw new PmtHouseError("pmthApiKey auth requires a non-empty apiKey", {
|
|
470
|
+
status: 400,
|
|
471
|
+
code: "invalid_gateway_token"
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
bundle.api_key = apiKey;
|
|
475
|
+
const billing = optionalString(input.auth.billing, "pmthApiKey billing URL");
|
|
476
|
+
if (billing) {
|
|
477
|
+
bundle.billing = billing;
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
const normalizedSignerHeaders = normalizeStringMap(signerHeaders);
|
|
481
|
+
if (normalizedSignerHeaders) {
|
|
482
|
+
bundle.signer_headers = normalizedSignerHeaders;
|
|
483
|
+
}
|
|
484
|
+
const normalizedDiscoveryHeaders = normalizeStringMap(input.discoveryHeaders);
|
|
485
|
+
if (normalizedDiscoveryHeaders) {
|
|
486
|
+
bundle.discovery_headers = normalizedDiscoveryHeaders;
|
|
487
|
+
}
|
|
488
|
+
return encodeBase64Json(bundle);
|
|
489
|
+
}
|
|
490
|
+
function decodeGatewayToken(token) {
|
|
491
|
+
const payload = decodeBase64Json(token);
|
|
492
|
+
if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
|
|
493
|
+
throw new PmtHouseError("Invalid gateway token: payload must be a JSON object", {
|
|
494
|
+
status: 400,
|
|
495
|
+
code: "invalid_gateway_token"
|
|
496
|
+
});
|
|
497
|
+
}
|
|
498
|
+
return payload;
|
|
499
|
+
}
|
|
500
|
+
async function mintGatewayToken(options) {
|
|
501
|
+
let accessToken;
|
|
502
|
+
if (options.source === "m2m") {
|
|
503
|
+
const minted = await mintUserSignerToken({
|
|
504
|
+
issuerUrl: options.issuerUrl,
|
|
505
|
+
m2mClientId: options.m2mClientId,
|
|
506
|
+
m2mClientSecret: options.m2mClientSecret,
|
|
507
|
+
externalUserId: options.externalUserId,
|
|
508
|
+
fetch: options.fetch,
|
|
509
|
+
allowInsecureHttp: options.allowInsecureHttp
|
|
510
|
+
});
|
|
511
|
+
accessToken = minted.jwt;
|
|
512
|
+
} else {
|
|
513
|
+
const minted = await mintSignerSessionFromApiKey({
|
|
514
|
+
issuerUrl: options.issuerUrl,
|
|
515
|
+
publicClientId: options.publicClientId,
|
|
516
|
+
m2mClientId: options.m2mClientId,
|
|
517
|
+
m2mClientSecret: options.m2mClientSecret,
|
|
518
|
+
apiKey: options.apiKey,
|
|
519
|
+
scope: options.scope,
|
|
520
|
+
audience: options.audience,
|
|
521
|
+
fetch: options.fetch,
|
|
522
|
+
allowInsecureHttp: options.allowInsecureHttp
|
|
523
|
+
});
|
|
524
|
+
accessToken = minted.access_token;
|
|
525
|
+
}
|
|
526
|
+
return buildGatewayToken({
|
|
527
|
+
signer: options.signer,
|
|
528
|
+
discovery: options.discovery,
|
|
529
|
+
orchestrators: options.orchestrators,
|
|
530
|
+
signerHeaders: options.signerHeaders,
|
|
531
|
+
discoveryHeaders: options.discoveryHeaders,
|
|
532
|
+
auth: { kind: "signerJwt", accessToken }
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
export { buildGatewayToken, decodeGatewayToken, mintGatewayToken };
|
|
537
|
+
//# sourceMappingURL=gateway.js.map
|
|
538
|
+
//# sourceMappingURL=gateway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/errors.ts","../../src/string-utils.ts","../../src/discovery.ts","../../src/encoding.ts","../../src/signer/fetch-json.ts","../../src/signer/json-fields.ts","../../src/signer/mint-token.ts","../../src/signer/device-exchange.ts","../../src/signer/api-key-exchange.ts","../../src/signer/gateway-token.ts"],"names":["EXCHANGE_RESPONSE_ERROR"],"mappings":"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","file":"gateway.js","sourcesContent":["export class PmtHouseError extends Error {\n readonly status: number;\n readonly code: string;\n readonly details?: unknown;\n\n constructor(\n message: string,\n {\n status = 500,\n code = \"pymthouse_error\",\n details,\n }: {\n status?: number;\n code?: string;\n details?: unknown;\n } = {},\n ) {\n super(message);\n this.name = \"PmtHouseError\";\n this.status = status;\n this.code = code;\n this.details = details;\n }\n}\n\nexport function toPmtHouseError(\n error: unknown,\n fallbackMessage: string,\n): PmtHouseError {\n if (error instanceof PmtHouseError) {\n return error;\n }\n\n if (error instanceof Error) {\n return new PmtHouseError(error.message || fallbackMessage, {\n code: \"unexpected_error\",\n status: 500,\n });\n }\n\n return new PmtHouseError(fallbackMessage, {\n code: \"unexpected_error\",\n status: 500,\n });\n}\n","/** Removes trailing `/` without regex (linear time). */\nexport function stripTrailingSlashes(value: string): string {\n let end = value.length;\n while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {\n end--;\n }\n return value.slice(0, end);\n}\n\nfunction endsWithIgnoreCase(value: string, suffix: string): boolean {\n if (suffix.length > value.length) {\n return false;\n }\n const start = value.length - suffix.length;\n for (let i = 0; i < suffix.length; i++) {\n const a = value.codePointAt(start + i) ?? 0;\n const b = suffix.codePointAt(i) ?? 0;\n if (a !== b && (a | 32) !== (b | 32)) {\n return false;\n }\n }\n return true;\n}\n\nfunction stripSuffixIgnoreCase(value: string, suffix: string): string {\n return endsWithIgnoreCase(value, suffix)\n ? value.slice(0, value.length - suffix.length)\n : value;\n}\n\n/** Issuer URL (`…/oidc`) → Builder API base (`…/api/v1`). Linear-time; no regex. */\nexport function stripOidcPathSuffix(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Issuer URL (`…/api/v1/oidc`) → host origin for signer/API-key routes. Linear-time; no regex. */\nexport function stripIssuerOriginFromOidcUrl(issuerUrl: string): string {\n let base = stripTrailingSlashes(issuerUrl.trim());\n base = stripSuffixIgnoreCase(base, \"/api/v1/oidc\");\n base = stripSuffixIgnoreCase(base, \"/oidc\");\n return stripTrailingSlashes(base);\n}\n\n/** Parse and validate an http(s) facade origin (no path). */\nexport function parseHttpOrigin(raw: string | undefined, fallback: string): string {\n const trimmed = (raw ?? fallback).trim();\n let parsed: URL;\n try {\n parsed = new URL(trimmed);\n } catch {\n throw new TypeError(\"Origin must be a valid http(s) URL\");\n }\n if (parsed.protocol !== \"http:\" && parsed.protocol !== \"https:\") {\n throw new TypeError(\"Origin must use http or https\");\n }\n return parsed.origin;\n}\n\n","import {\n allowInsecureRequests,\n customFetch,\n discoveryRequest,\n processDiscoveryResponse,\n type AuthorizationServer,\n} from \"oauth4webapi\";\nimport { PmtHouseError } from \"./errors.js\";\nimport { stripTrailingSlashes } from \"./string-utils.js\";\nimport type { FetchLike, OidcDiscoveryDocument } from \"./types.js\";\n\nexport function authorizationServerToOidcDocument(as: AuthorizationServer): OidcDiscoveryDocument {\n const tokenEndpoint = as.token_endpoint;\n const jwksUri = as.jwks_uri;\n if (!tokenEndpoint || !jwksUri) {\n throw new PmtHouseError(\"OIDC discovery document is missing token_endpoint or jwks_uri\", {\n status: 500,\n code: \"oidc_discovery_invalid\",\n });\n }\n return {\n issuer: as.issuer,\n authorization_endpoint: as.authorization_endpoint ?? \"\",\n token_endpoint: tokenEndpoint,\n jwks_uri: jwksUri,\n userinfo_endpoint: as.userinfo_endpoint,\n device_authorization_endpoint: as.device_authorization_endpoint,\n };\n}\n\nconst CACHE_TTL_MS = 5 * 60 * 1000;\n\ntype CacheEntry = {\n as: AuthorizationServer;\n fetchedAt: number;\n};\n\nconst discoveryCache = new Map<string, CacheEntry>();\n\nfunction normalizedIssuerKey(issuerUrl: string): string {\n return stripTrailingSlashes(issuerUrl);\n}\n\nexport interface LoadAuthorizationServerOptions {\n force?: boolean;\n allowInsecureHttp?: boolean;\n}\n\n/**\n * Loads OIDC discovery metadata via oauth4webapi (RFC 8414 / OIDC Discovery), with a 5-minute cache.\n */\nexport async function loadAuthorizationServer(\n issuerUrl: string,\n fetchImpl: FetchLike,\n options: LoadAuthorizationServerOptions = {},\n): Promise<AuthorizationServer> {\n const key = normalizedIssuerKey(issuerUrl);\n const now = Date.now();\n const cached = discoveryCache.get(key);\n\n if (!options.force && cached && now - cached.fetchedAt < CACHE_TTL_MS) {\n return cached.as;\n }\n\n const issuerIdentifier = new URL(key);\n const discoveryOpts: Parameters<typeof discoveryRequest>[1] = {\n algorithm: \"oidc\",\n [customFetch]: fetchImpl,\n };\n if (options.allowInsecureHttp) {\n discoveryOpts[allowInsecureRequests] = true;\n }\n\n let response: Response;\n try {\n response = await discoveryRequest(issuerIdentifier, discoveryOpts);\n } catch (e) {\n throw mapDiscoveryNetworkError(e);\n }\n\n let as: AuthorizationServer;\n try {\n as = await processDiscoveryResponse(issuerIdentifier, response);\n } catch (e) {\n throw mapOAuthDiscoveryError(e);\n }\n\n discoveryCache.set(key, { as, fetchedAt: now });\n return as;\n}\n\nexport async function fetchDiscoveryDocument(\n issuerUrl: string,\n fetchImpl: FetchLike,\n options: LoadAuthorizationServerOptions = {},\n): Promise<OidcDiscoveryDocument> {\n const as = await loadAuthorizationServer(issuerUrl, fetchImpl, options);\n return authorizationServerToOidcDocument(as);\n}\n\nexport function clearDiscoveryCache(issuerUrl?: string): void {\n if (!issuerUrl) {\n discoveryCache.clear();\n return;\n }\n discoveryCache.delete(normalizedIssuerKey(issuerUrl));\n}\n\nfunction mapOAuthDiscoveryError(error: unknown): PmtHouseError {\n if (error instanceof PmtHouseError) {\n return error;\n }\n if (error instanceof Error) {\n return new PmtHouseError(error.message, {\n status: 500,\n code: \"oidc_discovery_invalid\",\n details: { cause: error.cause },\n });\n }\n return new PmtHouseError(\"OIDC discovery failed\", {\n status: 500,\n code: \"oidc_discovery_invalid\",\n });\n}\n\nfunction mapDiscoveryNetworkError(error: unknown): PmtHouseError {\n if (error instanceof PmtHouseError) {\n return error;\n }\n if (error instanceof Error) {\n return new PmtHouseError(`Failed to load OIDC discovery: ${error.message}`, {\n status: 502,\n code: \"oidc_discovery_failed\",\n });\n }\n return new PmtHouseError(\"Failed to load OIDC discovery\", {\n status: 502,\n code: \"oidc_discovery_failed\",\n });\n}\n","/**\n * Base64url-safe Basic auth encoding for `client_id:client_secret` (UTF-8).\n * Works in Node, Edge, and Workers without assuming `Buffer`.\n */\nexport function encodeClientSecretBasic(clientId: string, clientSecret: string): string {\n const raw = `${clientId}:${clientSecret}`;\n const b64 =\n typeof Buffer !== \"undefined\"\n ? Buffer.from(raw, \"utf8\").toString(\"base64\")\n : btoa(Array.from(new TextEncoder().encode(raw), (c) => String.fromCharCode(c)).join(\"\"));\n return `Basic ${b64}`;\n}\n","import { PmtHouseError } from \"../errors.js\";\n\nfunction oauthFailureDescription(\n parsed: Record<string, unknown>,\n failureLabel: string,\n status: number,\n): string {\n if (typeof parsed.error_description === \"string\") {\n return parsed.error_description;\n }\n if (typeof parsed.error === \"string\") {\n return parsed.error;\n }\n return `${failureLabel} (${status})`;\n}\n\nexport type ReadJsonObjectFromResponseOptions = {\n invalidJsonMessage: string;\n invalidJsonCode: string;\n failureLabel: string;\n defaultErrorCode: string;\n};\n\nexport async function readJsonObjectFromResponse(\n response: Response,\n options: ReadJsonObjectFromResponseOptions,\n): Promise<Record<string, unknown>> {\n const text = await response.text();\n let parsed: Record<string, unknown>;\n try {\n parsed = text ? (JSON.parse(text) as Record<string, unknown>) : {};\n } catch {\n throw new PmtHouseError(options.invalidJsonMessage, {\n status: 502,\n code: options.invalidJsonCode,\n details: { status: response.status },\n });\n }\n\n if (!response.ok) {\n const description = oauthFailureDescription(parsed, options.failureLabel, response.status);\n throw new PmtHouseError(description, {\n status: response.status,\n code: typeof parsed.error === \"string\" ? parsed.error : options.defaultErrorCode,\n details: parsed,\n });\n }\n\n return parsed;\n}\n","import { PmtHouseError } from \"../errors.js\";\n\nexport function readStringField(\n body: Record<string, unknown>,\n key: string,\n errorCode: string,\n messagePrefix = \"Response\",\n): string {\n const value = body[key];\n if (typeof value !== \"string\" || !value.trim()) {\n throw new PmtHouseError(`${messagePrefix} missing ${key}`, {\n status: 502,\n code: errorCode,\n });\n }\n return value.trim();\n}\n\nexport function readExpiresIn(body: Record<string, unknown>, errorCode: string): number {\n const expiresIn = body.expires_in;\n if (typeof expiresIn !== \"number\" || !Number.isFinite(expiresIn) || expiresIn <= 0) {\n throw new PmtHouseError(\"Response missing expires_in\", {\n status: 502,\n code: errorCode,\n });\n }\n return Math.floor(expiresIn);\n}\n","import { stripTrailingSlashes } from \"../string-utils.js\";\nimport { loadAuthorizationServer } from \"../discovery.js\";\nimport { encodeClientSecretBasic } from \"../encoding.js\";\nimport { PmtHouseError } from \"../errors.js\";\nimport { readJsonObjectFromResponse } from \"./fetch-json.js\";\nimport { readExpiresIn, readStringField } from \"./json-fields.js\";\nimport type { CachedSignerToken, MintUserSignerTokenOptions, MintUserSignerTokenResponse } from \"./types.js\";\n\nexport const SIGN_MINT_USER_TOKEN_SCOPE = \"sign:mint_user_token\";\n\n/** @deprecated Signer JWT `aud` is the OIDC issuer URL; kept for legacy callers. */\nexport const LIVEPEER_REMOTE_SIGNER_AUDIENCE = \"livepeer-remote-signer\";\n\nconst DEFAULT_TTL_REFRESH_RATIO = 0.8;\nconst TOKEN_RESPONSE_ERROR = \"invalid_token_response\";\n\nexport function signerJwtAudience(issuerUrl: string): string {\n return stripTrailingSlashes(issuerUrl);\n}\n\nexport function parseMintUserSignerTokenResponse(\n body: Record<string, unknown>,\n ttlRefreshRatio = DEFAULT_TTL_REFRESH_RATIO,\n): CachedSignerToken {\n const accessToken = readStringField(body, \"access_token\", TOKEN_RESPONSE_ERROR, \"Token response\");\n const expiresIn = readExpiresIn(body, TOKEN_RESPONSE_ERROR);\n const balanceUsdMicros = readStringField(\n body,\n \"balanceUsdMicros\",\n TOKEN_RESPONSE_ERROR,\n \"Token response\",\n );\n const lifetimeGrantedUsdMicros = readStringField(\n body,\n \"lifetimeGrantedUsdMicros\",\n TOKEN_RESPONSE_ERROR,\n \"Token response\",\n );\n const now = Date.now();\n const expiresAt = now + expiresIn * 1000;\n const refreshAt = now + Math.floor(expiresIn * 1000 * ttlRefreshRatio);\n\n return {\n jwt: accessToken,\n expiresAt,\n refreshAt,\n balanceUsdMicros,\n lifetimeGrantedUsdMicros,\n };\n}\n\nexport async function mintUserSignerToken(\n options: MintUserSignerTokenOptions,\n): Promise<CachedSignerToken> {\n const fetchImpl = options.fetch ?? fetch;\n const issuerUrl = stripTrailingSlashes(options.issuerUrl);\n const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {\n allowInsecureHttp: options.allowInsecureHttp,\n });\n const tokenEndpoint = as.token_endpoint;\n if (!tokenEndpoint) {\n throw new PmtHouseError(\"OIDC discovery document is missing token_endpoint\", {\n status: 500,\n code: \"oidc_discovery_invalid\",\n });\n }\n\n const audience = signerJwtAudience(issuerUrl);\n const body = new URLSearchParams({\n grant_type: \"client_credentials\",\n scope: SIGN_MINT_USER_TOKEN_SCOPE,\n external_user_id: options.externalUserId,\n audience,\n });\n\n const response = await fetchImpl(tokenEndpoint, {\n method: \"POST\",\n headers: {\n Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body: body.toString(),\n cache: \"no-store\",\n });\n\n const parsed = await readJsonObjectFromResponse(response, {\n invalidJsonMessage: \"Token endpoint returned invalid JSON\",\n invalidJsonCode: TOKEN_RESPONSE_ERROR,\n failureLabel: \"Token mint failed\",\n defaultErrorCode: \"token_mint_failed\",\n });\n\n return parseMintUserSignerTokenResponse(parsed);\n}\n\nexport function toMintUserSignerTokenResponse(token: CachedSignerToken): MintUserSignerTokenResponse {\n const expiresIn = Math.max(1, Math.floor((token.expiresAt - Date.now()) / 1000));\n return {\n access_token: token.jwt,\n expires_in: expiresIn,\n balanceUsdMicros: token.balanceUsdMicros,\n lifetimeGrantedUsdMicros: token.lifetimeGrantedUsdMicros,\n };\n}\n","import { loadAuthorizationServer } from \"../discovery.js\";\nimport { encodeClientSecretBasic } from \"../encoding.js\";\nimport { PmtHouseError } from \"../errors.js\";\nimport { stripTrailingSlashes } from \"../string-utils.js\";\nimport { readJsonObjectFromResponse } from \"./fetch-json.js\";\nimport { readExpiresIn, readStringField } from \"./json-fields.js\";\nimport { signerHandlerErrorResponse } from \"./handler-errors.js\";\nimport { parseMintUserSignerTokenResponse } from \"./mint-token.js\";\nimport { assertDirectSignerBaseUrl } from \"./direct-signer.js\";\nimport type {\n DeviceExchangeHandlerConfig,\n DeviceExchangeHandlerConfigRemote,\n DeviceExchangeMintContext,\n DeviceExchangeMintResult,\n DeviceExchangeRequestBody,\n DeviceExchangeResponse,\n ExchangeDeviceTokenForSignerOptions,\n MintSignerTokenFromDeviceTokenOptions,\n} from \"./types.js\";\n\nconst TOKEN_EXCHANGE_GRANT = \"urn:ietf:params:oauth:grant-type:token-exchange\";\nconst SUBJECT_ACCESS_TOKEN_TYPE = \"urn:ietf:params:oauth:token-type:access_token\";\nconst EXCHANGE_RESPONSE_ERROR = \"invalid_exchange_response\";\n\nexport function extractSignerAccessTokenFromExchangeBody(\n body: Record<string, unknown>,\n): string {\n const tokenObj = body.token;\n if (tokenObj !== null && typeof tokenObj === \"object\" && !Array.isArray(tokenObj)) {\n const nested = tokenObj as Record<string, unknown>;\n for (const key of [\"accessToken\", \"access_token\"] as const) {\n const value = nested[key];\n if (typeof value === \"string\" && value.trim()) {\n return value.trim();\n }\n }\n }\n for (const key of [\"accessToken\", \"access_token\"] as const) {\n const value = body[key];\n if (typeof value === \"string\" && value.trim()) {\n return value.trim();\n }\n }\n throw new PmtHouseError(\"Device exchange response missing signer access token\", {\n status: 502,\n code: \"invalid_exchange_response\",\n });\n}\n\nexport function normalizeDeviceExchangeResponse(\n minted: DeviceExchangeMintResult,\n options?: { signerUrl?: string },\n): DeviceExchangeResponse {\n const scope = minted.scope.trim() || \"sign:job\";\n const body: DeviceExchangeResponse = {\n access_token: minted.access_token,\n token_type: \"Bearer\",\n expires_in: minted.expires_in,\n scope,\n balanceUsdMicros: minted.balanceUsdMicros,\n lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros,\n token: {\n accessToken: minted.access_token,\n access_token: minted.access_token,\n expiresIn: minted.expires_in,\n expires_in: minted.expires_in,\n scope,\n balanceUsdMicros: minted.balanceUsdMicros,\n lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros,\n },\n };\n const signerUrl = options?.signerUrl?.trim();\n if (signerUrl) {\n body.signerUrl = signerUrl;\n }\n return body;\n}\n\nexport async function parseDeviceExchangeRequestBody(\n request: Request,\n): Promise<DeviceExchangeRequestBody> {\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n throw new PmtHouseError(\"Request body must be JSON\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n if (body === null || typeof body !== \"object\" || Array.isArray(body)) {\n throw new PmtHouseError(\"Request body must be a JSON object\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n const record = body as Record<string, unknown>;\n const deviceTokenRaw = record.deviceToken;\n if (typeof deviceTokenRaw !== \"string\" || !deviceTokenRaw.trim()) {\n throw new PmtHouseError(\"Request body must include deviceToken\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n const deviceToken = deviceTokenRaw.trim();\n const scope =\n typeof record.scope === \"string\" && record.scope.trim()\n ? record.scope.trim()\n : undefined;\n const clientId =\n typeof record.clientId === \"string\" && record.clientId.trim()\n ? record.clientId.trim()\n : undefined;\n return { deviceToken, scope, clientId };\n}\n\nexport async function mintSignerTokenFromDeviceToken(\n options: MintSignerTokenFromDeviceTokenOptions,\n): Promise<DeviceExchangeMintResult> {\n const fetchImpl = options.fetch ?? fetch;\n const issuerUrl = stripTrailingSlashes(options.issuerUrl);\n const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {\n allowInsecureHttp: options.allowInsecureHttp,\n });\n const tokenEndpoint = as.token_endpoint;\n if (!tokenEndpoint) {\n throw new PmtHouseError(\"OIDC discovery document is missing token_endpoint\", {\n status: 500,\n code: \"oidc_discovery_invalid\",\n });\n }\n\n // Default the token-exchange audience/resource to the OIDC issuer URL, which is\n // what the IdP's token-exchange grant validates against (e.g. pymthouse's\n // signerJwtAudience()). The legacy \"livepeer-remote-signer\" literal is rejected\n // with invalid_target by current issuers.\n const audience = options.audience?.trim() || issuerUrl;\n const params = new URLSearchParams({\n grant_type: TOKEN_EXCHANGE_GRANT,\n subject_token: options.deviceToken,\n subject_token_type: SUBJECT_ACCESS_TOKEN_TYPE,\n audience,\n resource: audience,\n });\n if (options.scope?.trim()) {\n params.set(\"scope\", options.scope.trim());\n }\n\n const response = await fetchImpl(tokenEndpoint, {\n method: \"POST\",\n headers: {\n Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),\n \"Content-Type\": \"application/x-www-form-urlencoded\",\n Accept: \"application/json\",\n },\n body: params.toString(),\n cache: \"no-store\",\n });\n\n const parsed = await readJsonObjectFromResponse(response, {\n invalidJsonMessage: \"Token endpoint returned invalid JSON\",\n invalidJsonCode: \"invalid_token_response\",\n failureLabel: \"Signer JWT exchange failed\",\n defaultErrorCode: \"token_exchange_failed\",\n });\n\n const cached = parseMintUserSignerTokenResponse(parsed);\n return {\n access_token: cached.jwt,\n expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),\n scope: readStringField(parsed, \"scope\", EXCHANGE_RESPONSE_ERROR),\n balanceUsdMicros: cached.balanceUsdMicros,\n lifetimeGrantedUsdMicros: cached.lifetimeGrantedUsdMicros,\n };\n}\n\nexport async function exchangeDeviceTokenForSigner(\n options: ExchangeDeviceTokenForSignerOptions,\n): Promise<DeviceExchangeResponse> {\n const fetchImpl = options.fetch ?? fetch;\n const url = `${stripTrailingSlashes(options.facadeUrl)}/api/signer/device/exchange`;\n const body: Record<string, string> = { deviceToken: options.deviceToken };\n if (options.scope?.trim()) {\n body.scope = options.scope.trim();\n }\n if (options.clientId?.trim()) {\n body.clientId = options.clientId.trim();\n }\n\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(body),\n cache: \"no-store\",\n });\n\n const parsed = await readJsonObjectFromResponse(response, {\n invalidJsonMessage: \"Device exchange returned invalid JSON\",\n invalidJsonCode: EXCHANGE_RESPONSE_ERROR,\n failureLabel: \"Device exchange failed\",\n defaultErrorCode: \"device_exchange_failed\",\n });\n\n const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);\n const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;\n const signerUrl =\n typeof signerUrlRaw === \"string\" && signerUrlRaw.trim() ? signerUrlRaw.trim() : undefined;\n if (signerUrl) {\n assertDirectSignerBaseUrl(signerUrl);\n }\n return normalizeDeviceExchangeResponse(\n {\n access_token: accessToken,\n expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),\n scope:\n typeof parsed.scope === \"string\" && parsed.scope.trim()\n ? parsed.scope.trim()\n : \"sign:job\",\n balanceUsdMicros:\n typeof parsed.balanceUsdMicros === \"string\" ? parsed.balanceUsdMicros : \"0\",\n lifetimeGrantedUsdMicros:\n typeof parsed.lifetimeGrantedUsdMicros === \"string\"\n ? parsed.lifetimeGrantedUsdMicros\n : \"0\",\n },\n { signerUrl },\n );\n}\n\ntype CreateDeviceExchangeHandlerInput =\n | DeviceExchangeHandlerConfig\n | DeviceExchangeHandlerConfigRemote;\n\nfunction resolveMint(\n config: CreateDeviceExchangeHandlerInput,\n): (deviceToken: string, context: DeviceExchangeMintContext) => Promise<DeviceExchangeMintResult> {\n if (\"mint\" in config && typeof config.mint === \"function\") {\n return config.mint;\n }\n return (deviceToken, context) =>\n mintSignerTokenFromDeviceToken({\n issuerUrl: config.issuerUrl,\n m2mClientId: config.m2mClientId,\n m2mClientSecret: config.m2mClientSecret,\n deviceToken,\n scope: context.scope,\n audience: config.audience,\n fetch: config.fetch,\n allowInsecureHttp: config.allowInsecureHttp,\n });\n}\n\nfunction resolveSignerUrlFromConfig(\n config: CreateDeviceExchangeHandlerInput,\n): string | Promise<string | undefined> | undefined {\n if (\"signerUrl\" in config && typeof config.signerUrl === \"string\" && config.signerUrl.trim()) {\n return config.signerUrl.trim();\n }\n if (\"getSignerUrl\" in config && typeof config.getSignerUrl === \"function\") {\n return config.getSignerUrl();\n }\n return undefined;\n}\n\nexport function createDeviceExchangeHandler(\n config: CreateDeviceExchangeHandlerInput,\n): (request: Request) => Promise<Response> {\n const mint = resolveMint(config);\n\n return async function deviceExchangeHandler(request: Request): Promise<Response> {\n try {\n if (request.method !== \"POST\") {\n return new Response(JSON.stringify({ error: \"method_not_allowed\" }), {\n status: 405,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n const parsed = await parseDeviceExchangeRequestBody(request);\n const minted = await mint(parsed.deviceToken, {\n scope: parsed.scope,\n clientId: parsed.clientId,\n });\n const signerUrlValue = await resolveSignerUrlFromConfig(config);\n const body = normalizeDeviceExchangeResponse(minted, {\n signerUrl: typeof signerUrlValue === \"string\" ? signerUrlValue : undefined,\n });\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"no-store\",\n },\n });\n } catch (error) {\n return signerHandlerErrorResponse(error);\n }\n };\n}\n","import { stripIssuerOriginFromOidcUrl, stripTrailingSlashes } from \"../string-utils.js\";\nimport { PmtHouseError } from \"../errors.js\";\nimport { readJsonObjectFromResponse } from \"./fetch-json.js\";\nimport { signerHandlerErrorResponse } from \"./handler-errors.js\";\nimport {\n extractSignerAccessTokenFromExchangeBody,\n mintSignerTokenFromDeviceToken,\n normalizeDeviceExchangeResponse,\n} from \"./device-exchange.js\";\nimport { assertDirectSignerBaseUrl } from \"./direct-signer.js\";\nimport type {\n ApiKeyExchangeHandlerConfig,\n ApiKeyExchangeMintResult,\n ApiKeyExchangeRequestBody,\n DeviceExchangeResponse,\n ExchangeApiKeyForSignerOptions,\n} from \"./types.js\";\n\nconst EXCHANGE_RESPONSE_ERROR = \"invalid_exchange_response\";\n\nexport async function parseApiKeyExchangeRequestBody(\n request: Request,\n): Promise<ApiKeyExchangeRequestBody> {\n let body: unknown;\n try {\n body = await request.json();\n } catch {\n throw new PmtHouseError(\"Request body must be JSON\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n if (body === null || typeof body !== \"object\" || Array.isArray(body)) {\n throw new PmtHouseError(\"Request body must be a JSON object\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n const record = body as Record<string, unknown>;\n const apiKeyRaw = record.apiKey;\n if (typeof apiKeyRaw !== \"string\" || !apiKeyRaw.trim()) {\n throw new PmtHouseError(\"Request body must include apiKey\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n const scope =\n typeof record.scope === \"string\" && record.scope.trim()\n ? record.scope.trim()\n : undefined;\n const clientId =\n typeof record.clientId === \"string\" && record.clientId.trim()\n ? record.clientId.trim()\n : undefined;\n return { apiKey: apiKeyRaw.trim(), scope, clientId };\n}\n\nexport async function mintUserAccessTokenFromApiKey(input: {\n issuerUrl: string;\n publicClientId: string;\n apiKey: string;\n scope?: string;\n fetch?: typeof fetch;\n}): Promise<{ access_token: string; expires_in: number; scope: string }> {\n const fetchImpl = input.fetch ?? fetch;\n const issuerOrigin = stripIssuerOriginFromOidcUrl(input.issuerUrl);\n const url = `${issuerOrigin}/api/v1/apps/${encodeURIComponent(input.publicClientId)}/auth/api-key/token`;\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers: {\n Authorization: `Bearer ${input.apiKey}`,\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(input.scope ? { scope: input.scope } : {}),\n cache: \"no-store\",\n });\n\n const parsed = await readJsonObjectFromResponse(response, {\n invalidJsonMessage: \"API key token exchange returned invalid JSON\",\n invalidJsonCode: \"invalid_token_response\",\n failureLabel: \"API key token exchange failed\",\n defaultErrorCode: \"api_key_token_exchange_failed\",\n });\n\n const accessToken = parsed.access_token;\n if (typeof accessToken !== \"string\" || !accessToken.trim()) {\n throw new PmtHouseError(\"API key token exchange missing access_token\", {\n status: 502,\n code: EXCHANGE_RESPONSE_ERROR,\n });\n }\n\n const expiresIn =\n typeof parsed.expires_in === \"number\" && Number.isFinite(parsed.expires_in)\n ? parsed.expires_in\n : 900;\n const scope =\n typeof parsed.scope === \"string\" && parsed.scope.trim()\n ? parsed.scope.trim()\n : input.scope?.trim() || \"sign:job\";\n\n return {\n access_token: accessToken.trim(),\n expires_in: expiresIn,\n scope,\n };\n}\n\nexport async function mintSignerSessionFromApiKey(input: {\n issuerUrl: string;\n publicClientId: string;\n m2mClientId: string;\n m2mClientSecret: string;\n apiKey: string;\n scope?: string;\n audience?: string;\n fetch?: typeof fetch;\n allowInsecureHttp?: boolean;\n}): Promise<ApiKeyExchangeMintResult> {\n const userToken = await mintUserAccessTokenFromApiKey({\n issuerUrl: input.issuerUrl,\n publicClientId: input.publicClientId,\n apiKey: input.apiKey,\n scope: input.scope,\n fetch: input.fetch,\n });\n\n return mintSignerTokenFromDeviceToken({\n issuerUrl: input.issuerUrl,\n m2mClientId: input.m2mClientId,\n m2mClientSecret: input.m2mClientSecret,\n deviceToken: userToken.access_token,\n scope: userToken.scope,\n audience: input.audience,\n fetch: input.fetch,\n allowInsecureHttp: input.allowInsecureHttp,\n });\n}\n\nexport async function exchangeApiKeyForSigner(\n options: ExchangeApiKeyForSignerOptions,\n): Promise<DeviceExchangeResponse> {\n const fetchImpl = options.fetch ?? fetch;\n const url = `${stripTrailingSlashes(options.facadeUrl)}/api/pymthouse/keys/exchange`;\n const body: Record<string, string> = { apiKey: options.apiKey };\n if (options.scope?.trim()) {\n body.scope = options.scope.trim();\n }\n if (options.clientId?.trim()) {\n body.clientId = options.clientId.trim();\n }\n\n const response = await fetchImpl(url, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"application/json\",\n },\n body: JSON.stringify(body),\n cache: \"no-store\",\n });\n\n const parsed = await readJsonObjectFromResponse(response, {\n invalidJsonMessage: \"API key exchange returned invalid JSON\",\n invalidJsonCode: EXCHANGE_RESPONSE_ERROR,\n failureLabel: \"API key exchange failed\",\n defaultErrorCode: \"api_key_exchange_failed\",\n });\n\n const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);\n const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;\n const signerUrl =\n typeof signerUrlRaw === \"string\" && signerUrlRaw.trim() ? signerUrlRaw.trim() : undefined;\n if (signerUrl) {\n assertDirectSignerBaseUrl(signerUrl);\n }\n\n return normalizeDeviceExchangeResponse(\n {\n access_token: accessToken,\n expires_in:\n typeof parsed.expires_in === \"number\" && Number.isFinite(parsed.expires_in)\n ? parsed.expires_in\n : 3600,\n scope:\n typeof parsed.scope === \"string\" && parsed.scope.trim()\n ? parsed.scope.trim()\n : \"sign:job\",\n balanceUsdMicros:\n typeof parsed.balanceUsdMicros === \"string\" ? parsed.balanceUsdMicros : \"0\",\n lifetimeGrantedUsdMicros:\n typeof parsed.lifetimeGrantedUsdMicros === \"string\"\n ? parsed.lifetimeGrantedUsdMicros\n : \"0\",\n },\n { signerUrl },\n );\n}\n\nexport function createApiKeyExchangeHandler(\n config: ApiKeyExchangeHandlerConfig,\n): (request: Request) => Promise<Response> {\n const publicClientId = config.publicClientId.trim();\n\n return async function apiKeyExchangeHandler(request: Request): Promise<Response> {\n try {\n if (request.method !== \"POST\") {\n return new Response(JSON.stringify({ error: \"method_not_allowed\" }), {\n status: 405,\n headers: { \"Content-Type\": \"application/json\" },\n });\n }\n\n const parsed = await parseApiKeyExchangeRequestBody(request);\n const effectiveClientId = parsed.clientId?.trim() || publicClientId;\n if (effectiveClientId !== publicClientId) {\n throw new PmtHouseError(\"clientId does not match configured public client\", {\n status: 400,\n code: \"invalid_request\",\n });\n }\n\n const minted = await mintSignerSessionFromApiKey({\n issuerUrl: config.issuerUrl,\n publicClientId,\n m2mClientId: config.m2mClientId,\n m2mClientSecret: config.m2mClientSecret,\n apiKey: parsed.apiKey,\n scope: parsed.scope,\n audience: config.audience,\n fetch: config.fetch,\n allowInsecureHttp: config.allowInsecureHttp,\n });\n\n const signerUrlValue =\n typeof config.signerUrl === \"string\" && config.signerUrl.trim()\n ? config.signerUrl.trim()\n : undefined;\n\n const body = normalizeDeviceExchangeResponse(minted, { signerUrl: signerUrlValue });\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: {\n \"Content-Type\": \"application/json\",\n \"Cache-Control\": \"no-store\",\n },\n });\n } catch (error) {\n return signerHandlerErrorResponse(error);\n }\n };\n}\n","import { PmtHouseError } from \"../errors.js\";\nimport type { FetchLike } from \"../types.js\";\nimport { mintUserSignerToken } from \"./mint-token.js\";\nimport { mintSignerSessionFromApiKey } from \"./api-key-exchange.js\";\n\n/**\n * Decoded shape of a livepeer-python-gateway `--token` bundle. Mirrors the\n * fields the gateway reads in `token.py` (`parse_token`). All fields are\n * optional because the gateway omits absent keys.\n */\nexport interface GatewayTokenBundle {\n orchestrators?: string[];\n signer?: string;\n discovery?: string;\n signer_headers?: Record<string, string>;\n discovery_headers?: Record<string, string>;\n billing?: string;\n api_key?: string;\n}\n\n/**\n * How the gateway should authenticate to the remote signer.\n *\n * - `signerJwt`: caller has already minted a signer JWT; it is forwarded as\n * `signer_headers.Authorization = \"Bearer <jwt>\"`. The gateway only reads the\n * JWT `exp` and cannot refresh it on its own.\n * - `pmthApiKey`: the gateway holds a `pmth_*` API key plus the platform facade\n * origin (`billing`) and exchanges for a short-lived signer JWT via\n * `POST {billing}/api/pymthouse/keys/exchange`, then signs directly to signer.\n */\nexport type GatewayTokenAuth =\n | { kind: \"signerJwt\"; accessToken: string }\n | { kind: \"pmthApiKey\"; apiKey: string; billing?: string };\n\nexport interface GatewayTokenInput {\n /** Remote signer base URL the gateway signs against. */\n signer: string;\n discovery?: string;\n orchestrators?: string[];\n signerHeaders?: Record<string, string>;\n discoveryHeaders?: Record<string, string>;\n auth?: GatewayTokenAuth;\n}\n\n/**\n * Coerce an externally supplied value to a trimmed string, throwing a\n * consistent {@link PmtHouseError} (rather than a raw `TypeError`) when the\n * input is not a string. Used to guard public entry points against untyped\n * JavaScript callers.\n *\n * @param value - The value to validate.\n * @param label - Human-readable description of the field for the error message.\n * @returns The trimmed string value.\n * @throws {PmtHouseError} When `value` is not a string.\n */\nfunction requireString(value: unknown, label: string): string {\n if (typeof value !== \"string\") {\n throw new PmtHouseError(`${label} must be a string`, {\n status: 400,\n code: \"invalid_gateway_token\",\n });\n }\n return value.trim();\n}\n\n/**\n * Like {@link requireString} but treats `null`/`undefined` as \"not provided\"\n * (returning `undefined`) while still rejecting non-string values with a\n * consistent {@link PmtHouseError}.\n *\n * @param value - The optional value to validate.\n * @param label - Human-readable description of the field for the error message.\n * @returns The trimmed string, or `undefined` when absent/empty.\n * @throws {PmtHouseError} When `value` is present but not a string.\n */\nfunction optionalString(value: unknown, label: string): string | undefined {\n if (value === undefined || value === null) {\n return undefined;\n }\n return requireString(value, label) || undefined;\n}\n\n/** Serialize a value to JSON and standard (non-url-safe) base64. */\nfunction encodeBase64Json(value: unknown): string {\n const json = JSON.stringify(value);\n if (typeof Buffer === \"undefined\") {\n const binary = Array.from(new TextEncoder().encode(json), (c) =>\n String.fromCodePoint(c),\n ).join(\"\");\n return btoa(binary);\n }\n return Buffer.from(json, \"utf8\").toString(\"base64\");\n}\n\n/** Decode a standard base64 string back into the parsed JSON value it encoded. */\nfunction decodeBase64Json(token: string): unknown {\n const trimmed = requireString(token, \"gateway token\");\n let json: string;\n try {\n if (typeof Buffer === \"undefined\") {\n json = new TextDecoder().decode(\n Uint8Array.from(atob(trimmed), (c) => c.codePointAt(0) ?? 0),\n );\n } else {\n json = Buffer.from(trimmed, \"base64\").toString(\"utf8\");\n }\n } catch {\n throw new PmtHouseError(\"Invalid gateway token: expected base64-encoded JSON\", {\n status: 400,\n code: \"invalid_gateway_token\",\n });\n }\n try {\n return JSON.parse(json);\n } catch {\n throw new PmtHouseError(\"Invalid gateway token: expected UTF-8 JSON payload\", {\n status: 400,\n code: \"invalid_gateway_token\",\n });\n }\n}\n\n/** Drop blank keys and non-string values from a header map, returning undefined when empty. */\nfunction normalizeStringMap(\n map: Record<string, string> | undefined,\n): Record<string, string> | undefined {\n if (!map) {\n return undefined;\n }\n const entries = Object.entries(map).filter(\n ([key, value]) => key.trim() && typeof value === \"string\",\n );\n return entries.length > 0 ? Object.fromEntries(entries) : undefined;\n}\n\n/**\n * Assemble a base64-encoded `--token` bundle the python gateway understands.\n * Pure (no network): builds the JSON object, omitting empty fields, then\n * base64-encodes it.\n */\nexport function buildGatewayToken(input: GatewayTokenInput): string {\n if (input === null || typeof input !== \"object\") {\n throw new PmtHouseError(\"buildGatewayToken requires an input object\", {\n status: 400,\n code: \"invalid_gateway_token\",\n });\n }\n const signer = requireString(input.signer, \"signer URL\");\n if (!signer) {\n throw new PmtHouseError(\"buildGatewayToken requires a non-empty signer URL\", {\n status: 400,\n code: \"invalid_gateway_token\",\n });\n }\n\n const signerHeaders: Record<string, string> = { ...input.signerHeaders };\n const bundle: GatewayTokenBundle = { signer };\n\n const discovery = optionalString(input.discovery, \"discovery URL\");\n if (discovery) {\n bundle.discovery = discovery;\n }\n\n const rawOrchestrators = input.orchestrators ?? [];\n if (!Array.isArray(rawOrchestrators)) {\n throw new PmtHouseError(\"orchestrators must be an array of strings\", {\n status: 400,\n code: \"invalid_gateway_token\",\n });\n }\n const orchestrators = rawOrchestrators\n .map((entry) => requireString(entry, \"orchestrator entry\"))\n .filter((entry) => entry.length > 0);\n if (orchestrators.length > 0) {\n bundle.orchestrators = orchestrators;\n }\n\n if (input.auth?.kind === \"signerJwt\") {\n const accessToken = requireString(input.auth.accessToken, \"signerJwt accessToken\");\n if (!accessToken) {\n throw new PmtHouseError(\"signerJwt auth requires a non-empty accessToken\", {\n status: 400,\n code: \"invalid_gateway_token\",\n });\n }\n signerHeaders.Authorization = `Bearer ${accessToken}`;\n } else if (input.auth?.kind === \"pmthApiKey\") {\n const apiKey = requireString(input.auth.apiKey, \"pmthApiKey apiKey\");\n if (!apiKey) {\n throw new PmtHouseError(\"pmthApiKey auth requires a non-empty apiKey\", {\n status: 400,\n code: \"invalid_gateway_token\",\n });\n }\n bundle.api_key = apiKey;\n const billing = optionalString(input.auth.billing, \"pmthApiKey billing URL\");\n if (billing) {\n bundle.billing = billing;\n }\n }\n\n const normalizedSignerHeaders = normalizeStringMap(signerHeaders);\n if (normalizedSignerHeaders) {\n bundle.signer_headers = normalizedSignerHeaders;\n }\n const normalizedDiscoveryHeaders = normalizeStringMap(input.discoveryHeaders);\n if (normalizedDiscoveryHeaders) {\n bundle.discovery_headers = normalizedDiscoveryHeaders;\n }\n\n return encodeBase64Json(bundle);\n}\n\n/** Inverse of {@link buildGatewayToken}; for tests and debugging. */\nexport function decodeGatewayToken(token: string): GatewayTokenBundle {\n const payload = decodeBase64Json(token);\n if (payload === null || typeof payload !== \"object\" || Array.isArray(payload)) {\n throw new PmtHouseError(\"Invalid gateway token: payload must be a JSON object\", {\n status: 400,\n code: \"invalid_gateway_token\",\n });\n }\n return payload as GatewayTokenBundle;\n}\n\ninterface MintGatewayTokenBase {\n /** Remote signer base URL the gateway signs against. */\n signer: string;\n discovery?: string;\n orchestrators?: string[];\n signerHeaders?: Record<string, string>;\n discoveryHeaders?: Record<string, string>;\n issuerUrl: string;\n m2mClientId: string;\n m2mClientSecret: string;\n fetch?: FetchLike;\n allowInsecureHttp?: boolean;\n}\n\nexport type MintGatewayTokenOptions = MintGatewayTokenBase &\n (\n | { source: \"m2m\"; externalUserId: string }\n | {\n source: \"apiKey\";\n publicClientId: string;\n apiKey: string;\n scope?: string;\n audience?: string;\n }\n );\n\n/**\n * Convenience: mint a signer JWT (either from M2M `client_credentials` or by\n * exchanging a `pmth_*` API key) and assemble a `signerJwt`-mode gateway token.\n */\nexport async function mintGatewayToken(\n options: MintGatewayTokenOptions,\n): Promise<string> {\n let accessToken: string;\n if (options.source === \"m2m\") {\n const minted = await mintUserSignerToken({\n issuerUrl: options.issuerUrl,\n m2mClientId: options.m2mClientId,\n m2mClientSecret: options.m2mClientSecret,\n externalUserId: options.externalUserId,\n fetch: options.fetch,\n allowInsecureHttp: options.allowInsecureHttp,\n });\n accessToken = minted.jwt;\n } else {\n const minted = await mintSignerSessionFromApiKey({\n issuerUrl: options.issuerUrl,\n publicClientId: options.publicClientId,\n m2mClientId: options.m2mClientId,\n m2mClientSecret: options.m2mClientSecret,\n apiKey: options.apiKey,\n scope: options.scope,\n audience: options.audience,\n fetch: options.fetch,\n allowInsecureHttp: options.allowInsecureHttp,\n });\n accessToken = minted.access_token;\n }\n\n return buildGatewayToken({\n signer: options.signer,\n discovery: options.discovery,\n orchestrators: options.orchestrators,\n signerHeaders: options.signerHeaders,\n discoveryHeaders: options.discoveryHeaders,\n auth: { kind: \"signerJwt\", accessToken },\n });\n}\n"]}
|