@pymthouse/builder-sdk 0.1.0 → 0.3.1
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 +66 -0
- package/dist/{client-BroVFyIy.d.ts → client-BHfjDvIe.d.ts} +49 -1
- package/dist/{client-BhC1YhB1.d.cts → client-CvhJEhjV.d.cts} +49 -1
- package/dist/config.cjs +59 -3
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +8 -1
- package/dist/config.d.ts +8 -1
- package/dist/config.js +57 -4
- package/dist/config.js.map +1 -1
- package/dist/device-initiate.cjs +1 -1
- package/dist/device-initiate.cjs.map +1 -1
- package/dist/device-initiate.js +1 -1
- package/dist/device-initiate.js.map +1 -1
- package/dist/device.cjs +1 -1
- package/dist/device.cjs.map +1 -1
- package/dist/device.d.cts +1 -1
- package/dist/device.d.ts +1 -1
- package/dist/device.js +1 -1
- package/dist/device.js.map +1 -1
- package/dist/env.cjs +794 -36
- 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 +794 -36
- package/dist/env.js.map +1 -1
- package/dist/gateway/client/index.cjs +492 -0
- package/dist/gateway/client/index.cjs.map +1 -0
- package/dist/gateway/client/index.d.cts +63 -0
- package/dist/gateway/client/index.d.ts +63 -0
- package/dist/gateway/client/index.js +489 -0
- package/dist/gateway/client/index.js.map +1 -0
- package/dist/gateway/index.cjs +16 -0
- package/dist/gateway/index.cjs.map +1 -0
- package/dist/gateway/index.d.cts +52 -0
- package/dist/gateway/index.d.ts +52 -0
- package/dist/gateway/index.js +10 -0
- package/dist/gateway/index.js.map +1 -0
- package/dist/gateway/server/index.cjs +1248 -0
- package/dist/gateway/server/index.cjs.map +1 -0
- package/dist/gateway/server/index.d.cts +31 -0
- package/dist/gateway/server/index.d.ts +31 -0
- package/dist/gateway/server/index.js +1233 -0
- package/dist/gateway/server/index.js.map +1 -0
- package/dist/index.cjs +1075 -186
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +6 -4
- package/dist/index.d.ts +6 -4
- package/dist/index.js +1042 -163
- package/dist/index.js.map +1 -1
- package/dist/ingest-B3Yi8Tb1.d.cts +271 -0
- package/dist/ingest-DoKJTWU9.d.ts +271 -0
- package/dist/plan-pricing.cjs +108 -0
- package/dist/plan-pricing.cjs.map +1 -0
- package/dist/plan-pricing.d.cts +15 -0
- package/dist/plan-pricing.d.ts +15 -0
- package/dist/plan-pricing.js +98 -0
- package/dist/plan-pricing.js.map +1 -0
- package/dist/signer/server.cjs +1366 -0
- package/dist/signer/server.cjs.map +1 -0
- package/dist/signer/server.d.cts +73 -0
- package/dist/signer/server.d.ts +73 -0
- package/dist/signer/server.js +1331 -0
- package/dist/signer/server.js.map +1 -0
- package/dist/tokens.d.cts +1 -1
- package/dist/tokens.d.ts +1 -1
- package/dist/types-_R1AwEZp.d.cts +343 -0
- package/dist/types-_R1AwEZp.d.ts +343 -0
- package/dist/verify.cjs +1 -1
- package/dist/verify.cjs.map +1 -1
- package/dist/verify.d.cts +1 -1
- package/dist/verify.d.ts +1 -1
- package/dist/verify.js +1 -1
- package/dist/verify.js.map +1 -1
- package/gateway/proto/lp_rpc.proto +542 -0
- package/package.json +42 -1
- package/dist/types-rKzVXvMu.d.cts +0 -196
- package/dist/types-rKzVXvMu.d.ts +0 -196
|
@@ -0,0 +1,1331 @@
|
|
|
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/signer/handler-errors.ts
|
|
22
|
+
function signerHandlerErrorResponse(error) {
|
|
23
|
+
if (error instanceof PmtHouseError) {
|
|
24
|
+
return new Response(
|
|
25
|
+
JSON.stringify({
|
|
26
|
+
error: error.code,
|
|
27
|
+
error_description: error.message,
|
|
28
|
+
details: error.details
|
|
29
|
+
}),
|
|
30
|
+
{
|
|
31
|
+
status: error.status,
|
|
32
|
+
headers: { "Content-Type": "application/json" }
|
|
33
|
+
}
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const message = error instanceof Error ? error.message : "Internal error";
|
|
37
|
+
return new Response(JSON.stringify({ error: "internal_error", error_description: message }), {
|
|
38
|
+
status: 500,
|
|
39
|
+
headers: { "Content-Type": "application/json" }
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// src/signer/token-manager.ts
|
|
44
|
+
function cacheKey(clientId, externalUserId) {
|
|
45
|
+
return `${clientId}\0${externalUserId}`;
|
|
46
|
+
}
|
|
47
|
+
function createSignerTokenManager(options) {
|
|
48
|
+
const ttlRefreshRatio = options.ttlRefreshRatio ?? 0.8;
|
|
49
|
+
const cache = /* @__PURE__ */ new Map();
|
|
50
|
+
const inflight = /* @__PURE__ */ new Map();
|
|
51
|
+
function keyFor(externalUserId) {
|
|
52
|
+
return cacheKey(options.publicClientId, externalUserId);
|
|
53
|
+
}
|
|
54
|
+
function isUsable(entry, now, forceRefresh) {
|
|
55
|
+
if (forceRefresh) return false;
|
|
56
|
+
if (now >= entry.expiresAt) return false;
|
|
57
|
+
if (now >= entry.refreshAt) return false;
|
|
58
|
+
return true;
|
|
59
|
+
}
|
|
60
|
+
async function refresh(externalUserId) {
|
|
61
|
+
const key = keyFor(externalUserId);
|
|
62
|
+
const existing = inflight.get(key);
|
|
63
|
+
if (existing) {
|
|
64
|
+
return existing;
|
|
65
|
+
}
|
|
66
|
+
const promise = options.mint(externalUserId).then((token) => {
|
|
67
|
+
const normalized = {
|
|
68
|
+
...token,
|
|
69
|
+
refreshAt: token.refreshAt || Date.now() + Math.floor((token.expiresAt - Date.now()) * ttlRefreshRatio)
|
|
70
|
+
};
|
|
71
|
+
cache.set(key, normalized);
|
|
72
|
+
inflight.delete(key);
|
|
73
|
+
return normalized;
|
|
74
|
+
}).catch((error) => {
|
|
75
|
+
inflight.delete(key);
|
|
76
|
+
throw error;
|
|
77
|
+
});
|
|
78
|
+
inflight.set(key, promise);
|
|
79
|
+
return promise;
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
peek(externalUserId) {
|
|
83
|
+
return cache.get(keyFor(externalUserId));
|
|
84
|
+
},
|
|
85
|
+
invalidate(externalUserId) {
|
|
86
|
+
const key = keyFor(externalUserId);
|
|
87
|
+
cache.delete(key);
|
|
88
|
+
inflight.delete(key);
|
|
89
|
+
},
|
|
90
|
+
async getToken(externalUserId, getOptions = {}) {
|
|
91
|
+
const now = Date.now();
|
|
92
|
+
const key = keyFor(externalUserId);
|
|
93
|
+
const cached = cache.get(key);
|
|
94
|
+
if (cached && isUsable(cached, now, getOptions.forceRefresh === true)) {
|
|
95
|
+
return cached;
|
|
96
|
+
}
|
|
97
|
+
return refresh(externalUserId);
|
|
98
|
+
}
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// src/signer/forward.ts
|
|
103
|
+
function base64UrlPayloadToUtf8(payloadB64) {
|
|
104
|
+
const normalized = payloadB64.replaceAll("-", "+").replaceAll("_", "/");
|
|
105
|
+
const padded = normalized.padEnd(Math.ceil(normalized.length / 4) * 4, "=");
|
|
106
|
+
if (typeof Buffer !== "undefined") {
|
|
107
|
+
return Buffer.from(padded, "base64").toString("utf8");
|
|
108
|
+
}
|
|
109
|
+
return atob(padded);
|
|
110
|
+
}
|
|
111
|
+
function decodeJwtPayload(jwt) {
|
|
112
|
+
const parts = jwt.trim().split(".");
|
|
113
|
+
if (parts.length < 2) {
|
|
114
|
+
throw new PmtHouseError("Invalid JWT shape", {
|
|
115
|
+
status: 500,
|
|
116
|
+
code: "invalid_jwt"
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
try {
|
|
120
|
+
const payloadJson = base64UrlPayloadToUtf8(parts[1]);
|
|
121
|
+
const payload = JSON.parse(payloadJson);
|
|
122
|
+
if (!payload || typeof payload !== "object") {
|
|
123
|
+
throw new Error("payload not an object");
|
|
124
|
+
}
|
|
125
|
+
return payload;
|
|
126
|
+
} catch {
|
|
127
|
+
throw new PmtHouseError("Failed to decode JWT payload", {
|
|
128
|
+
status: 500,
|
|
129
|
+
code: "invalid_jwt"
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function readClaim(payload, ...keys) {
|
|
134
|
+
for (const key of keys) {
|
|
135
|
+
const value = payload[key];
|
|
136
|
+
if (typeof value === "string" && value.trim()) {
|
|
137
|
+
return value.trim();
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return "";
|
|
141
|
+
}
|
|
142
|
+
function identityFromJwtPayload(payload) {
|
|
143
|
+
const issuer = readClaim(payload, "iss");
|
|
144
|
+
const clientId = readClaim(payload, "client_id");
|
|
145
|
+
const usageSubject = readClaim(payload, "external_user_id", "usage_subject", "sub");
|
|
146
|
+
const usageSubjectType = readClaim(payload, "external_user_id_type", "usage_subject_type") || "external_user_id";
|
|
147
|
+
if (!issuer || !clientId || !usageSubject) {
|
|
148
|
+
throw new PmtHouseError("JWT payload missing signer identity claims", {
|
|
149
|
+
status: 500,
|
|
150
|
+
code: "invalid_jwt",
|
|
151
|
+
details: { issuer, clientId, usageSubject }
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return {
|
|
155
|
+
issuer,
|
|
156
|
+
clientId,
|
|
157
|
+
usageSubject,
|
|
158
|
+
usageSubjectType
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
function livepeerIdentityHeaders(identity) {
|
|
162
|
+
return {
|
|
163
|
+
"X-Livepeer-Usage-Issuer": identity.issuer,
|
|
164
|
+
"X-Livepeer-Client-ID": identity.clientId,
|
|
165
|
+
"X-Livepeer-Usage-Subject": identity.usageSubject,
|
|
166
|
+
"X-Livepeer-Usage-Subject-Type": identity.usageSubjectType
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
function resolveRemoteUrl(request, remoteSignerUrl, proxyPathPrefix, defaultRemotePath = "/generate-live-payment") {
|
|
170
|
+
const remoteBase = new URL(remoteSignerUrl);
|
|
171
|
+
const incoming = new URL(request.url);
|
|
172
|
+
let remotePath = incoming.pathname;
|
|
173
|
+
if (proxyPathPrefix) {
|
|
174
|
+
const prefix = proxyPathPrefix.endsWith("/") ? proxyPathPrefix.slice(0, -1) : proxyPathPrefix;
|
|
175
|
+
if (remotePath.startsWith(prefix)) {
|
|
176
|
+
remotePath = remotePath.slice(prefix.length) || defaultRemotePath;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
if (!remotePath.startsWith("/")) {
|
|
180
|
+
remotePath = `/${remotePath}`;
|
|
181
|
+
}
|
|
182
|
+
const target = new URL(remoteBase);
|
|
183
|
+
target.pathname = remotePath || defaultRemotePath;
|
|
184
|
+
target.search = incoming.search;
|
|
185
|
+
return target;
|
|
186
|
+
}
|
|
187
|
+
async function forwardDirectSignerRequest(options) {
|
|
188
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
189
|
+
const identity = identityFromJwtPayload(decodeJwtPayload(options.jwt));
|
|
190
|
+
const target = resolveRemoteUrl(
|
|
191
|
+
options.request,
|
|
192
|
+
options.remoteSignerUrl,
|
|
193
|
+
options.proxyPathPrefix,
|
|
194
|
+
options.defaultRemotePath
|
|
195
|
+
);
|
|
196
|
+
const headers = new Headers(options.request.headers);
|
|
197
|
+
headers.set("Authorization", `Bearer ${options.jwt}`);
|
|
198
|
+
for (const [name, value] of Object.entries(livepeerIdentityHeaders(identity))) {
|
|
199
|
+
headers.set(name, value);
|
|
200
|
+
}
|
|
201
|
+
headers.delete("host");
|
|
202
|
+
headers.delete("content-length");
|
|
203
|
+
const init = {
|
|
204
|
+
method: options.request.method,
|
|
205
|
+
headers,
|
|
206
|
+
body: options.request.body,
|
|
207
|
+
cache: "no-store"
|
|
208
|
+
};
|
|
209
|
+
if (options.request.body) {
|
|
210
|
+
init.duplex = "half";
|
|
211
|
+
}
|
|
212
|
+
return fetchImpl(target, init);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// src/encoding.ts
|
|
216
|
+
function encodeClientSecretBasic(clientId, clientSecret) {
|
|
217
|
+
const raw = `${clientId}:${clientSecret}`;
|
|
218
|
+
const b64 = typeof Buffer !== "undefined" ? Buffer.from(raw, "utf8").toString("base64") : btoa(Array.from(new TextEncoder().encode(raw), (c) => String.fromCharCode(c)).join(""));
|
|
219
|
+
return `Basic ${b64}`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// src/string-utils.ts
|
|
223
|
+
function stripTrailingSlashes(value) {
|
|
224
|
+
let end = value.length;
|
|
225
|
+
while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
|
|
226
|
+
end--;
|
|
227
|
+
}
|
|
228
|
+
return value.slice(0, end);
|
|
229
|
+
}
|
|
230
|
+
function endsWithIgnoreCase(value, suffix) {
|
|
231
|
+
if (suffix.length > value.length) {
|
|
232
|
+
return false;
|
|
233
|
+
}
|
|
234
|
+
const start = value.length - suffix.length;
|
|
235
|
+
for (let i = 0; i < suffix.length; i++) {
|
|
236
|
+
const a = value.codePointAt(start + i) ?? 0;
|
|
237
|
+
const b = suffix.codePointAt(i) ?? 0;
|
|
238
|
+
if (a !== b && (a | 32) !== (b | 32)) {
|
|
239
|
+
return false;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
function stripSuffixIgnoreCase(value, suffix) {
|
|
245
|
+
return endsWithIgnoreCase(value, suffix) ? value.slice(0, value.length - suffix.length) : value;
|
|
246
|
+
}
|
|
247
|
+
function stripIssuerOriginFromOidcUrl(issuerUrl) {
|
|
248
|
+
let base = stripTrailingSlashes(issuerUrl.trim());
|
|
249
|
+
base = stripSuffixIgnoreCase(base, "/api/v1/oidc");
|
|
250
|
+
base = stripSuffixIgnoreCase(base, "/oidc");
|
|
251
|
+
return stripTrailingSlashes(base);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// src/ingest.ts
|
|
255
|
+
function signerSnapshotToIngestPayload(input) {
|
|
256
|
+
return {
|
|
257
|
+
requestId: input.snapshot.requestId,
|
|
258
|
+
externalUserId: input.externalUserId,
|
|
259
|
+
networkFeeUsdMicros: input.snapshot.computedFeeUsdMicros.toString(),
|
|
260
|
+
feeWei: input.snapshot.computedFeeWei,
|
|
261
|
+
pixels: input.snapshot.pixels,
|
|
262
|
+
pipeline: input.snapshot.pipeline,
|
|
263
|
+
modelId: input.snapshot.modelId,
|
|
264
|
+
gatewayRequestId: input.gatewayRequestId,
|
|
265
|
+
ethUsdPrice: input.snapshot.ethUsdPrice,
|
|
266
|
+
ethUsdRoundId: input.snapshot.ethUsdRoundId,
|
|
267
|
+
ethUsdObservedAt: input.snapshot.ethUsdObservedAt
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
function ingestUrl(issuerUrl, publicClientId) {
|
|
271
|
+
const origin = new URL(stripTrailingSlashes(issuerUrl)).origin;
|
|
272
|
+
return `${origin}/api/v1/apps/${encodeURIComponent(publicClientId)}/usage/signed-tickets`;
|
|
273
|
+
}
|
|
274
|
+
async function readJsonResponse(response) {
|
|
275
|
+
const text = await response.text();
|
|
276
|
+
if (!text.trim()) {
|
|
277
|
+
return {};
|
|
278
|
+
}
|
|
279
|
+
try {
|
|
280
|
+
const parsed = JSON.parse(text);
|
|
281
|
+
return typeof parsed === "object" && parsed !== null && !Array.isArray(parsed) ? parsed : {};
|
|
282
|
+
} catch {
|
|
283
|
+
return {};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
async function ingestSignedTicket(options) {
|
|
287
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
288
|
+
const url = ingestUrl(options.issuerUrl, options.publicClientId);
|
|
289
|
+
const response = await fetchImpl(url, {
|
|
290
|
+
method: "POST",
|
|
291
|
+
headers: {
|
|
292
|
+
Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),
|
|
293
|
+
"Content-Type": "application/json",
|
|
294
|
+
Accept: "application/json"
|
|
295
|
+
},
|
|
296
|
+
body: JSON.stringify(options.ticket),
|
|
297
|
+
cache: "no-store"
|
|
298
|
+
});
|
|
299
|
+
const body = await readJsonResponse(response);
|
|
300
|
+
if (!response.ok) {
|
|
301
|
+
const message = typeof body.error === "string" ? body.error : `Signed-ticket ingest failed (${response.status})`;
|
|
302
|
+
throw new PmtHouseError(message, {
|
|
303
|
+
status: response.status,
|
|
304
|
+
code: "ingest_failed",
|
|
305
|
+
details: body
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
return {
|
|
309
|
+
ingested: Boolean(body.ingested),
|
|
310
|
+
duplicate: Boolean(body.duplicate),
|
|
311
|
+
source: body.source === "openmeter" ? "openmeter" : "disabled"
|
|
312
|
+
};
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
// src/signer/proxy.ts
|
|
316
|
+
var HTTP_DMZ_TOKEN_MAX_ENTRIES = 100;
|
|
317
|
+
var HTTP_DMZ_TOKEN_TTL_MS = 3.5 * 60 * 1e3;
|
|
318
|
+
var DEFAULT_PROBE_SUBJECT = "signer-reachability-probe";
|
|
319
|
+
var httpDmzTokenCache = /* @__PURE__ */ new Map();
|
|
320
|
+
function normalizeSignerBaseUrl(base) {
|
|
321
|
+
return stripTrailingSlashes(base);
|
|
322
|
+
}
|
|
323
|
+
function joinSignerUrl(baseUrl, path) {
|
|
324
|
+
if (path.startsWith("/")) {
|
|
325
|
+
return `${baseUrl}${path}`;
|
|
326
|
+
}
|
|
327
|
+
return `${baseUrl}/${path}`;
|
|
328
|
+
}
|
|
329
|
+
function aliasBodyString(raw) {
|
|
330
|
+
if (raw === void 0 || raw === null) {
|
|
331
|
+
return null;
|
|
332
|
+
}
|
|
333
|
+
if (typeof raw === "string") {
|
|
334
|
+
return raw.length > 0 ? raw : null;
|
|
335
|
+
}
|
|
336
|
+
if (typeof raw === "number" || typeof raw === "boolean" || typeof raw === "bigint") {
|
|
337
|
+
return String(raw);
|
|
338
|
+
}
|
|
339
|
+
return null;
|
|
340
|
+
}
|
|
341
|
+
function resolveSignerBaseUrl(input) {
|
|
342
|
+
if (input.testSignerUrl?.trim()) {
|
|
343
|
+
return normalizeSignerBaseUrl(input.testSignerUrl);
|
|
344
|
+
}
|
|
345
|
+
const legacyBareSignerPort = 8081;
|
|
346
|
+
const rawPort = input.storedPort ?? input.defaultPort ?? 8080;
|
|
347
|
+
const port = rawPort === legacyBareSignerPort ? 8080 : rawPort;
|
|
348
|
+
const base = input.envUrl?.trim() || input.storedUrl?.trim() || `http://127.0.0.1:${port}`;
|
|
349
|
+
return normalizeSignerBaseUrl(base);
|
|
350
|
+
}
|
|
351
|
+
async function getCachedDmzBearerToken(subject, gate, getDmzToken) {
|
|
352
|
+
const cacheKey2 = `${gate}:${subject}`;
|
|
353
|
+
const now = Date.now();
|
|
354
|
+
const cached = httpDmzTokenCache.get(cacheKey2);
|
|
355
|
+
if (cached && cached.expMs > now + 15e3) {
|
|
356
|
+
httpDmzTokenCache.delete(cacheKey2);
|
|
357
|
+
httpDmzTokenCache.set(cacheKey2, cached);
|
|
358
|
+
return cached.token;
|
|
359
|
+
}
|
|
360
|
+
const token = await getDmzToken(subject, gate);
|
|
361
|
+
httpDmzTokenCache.set(cacheKey2, { token, expMs: now + HTTP_DMZ_TOKEN_TTL_MS });
|
|
362
|
+
if (httpDmzTokenCache.size > HTTP_DMZ_TOKEN_MAX_ENTRIES) {
|
|
363
|
+
const oldest = httpDmzTokenCache.keys().next().value;
|
|
364
|
+
if (oldest !== void 0) {
|
|
365
|
+
httpDmzTokenCache.delete(oldest);
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
return token;
|
|
369
|
+
}
|
|
370
|
+
async function readSignerUpstreamBody(response) {
|
|
371
|
+
const text = await response.text();
|
|
372
|
+
if (!text.trim()) {
|
|
373
|
+
return {};
|
|
374
|
+
}
|
|
375
|
+
try {
|
|
376
|
+
return JSON.parse(text);
|
|
377
|
+
} catch {
|
|
378
|
+
return {
|
|
379
|
+
error: "Signer DMZ returned a non-JSON body (often Apache auth failure)",
|
|
380
|
+
upstreamStatus: response.status,
|
|
381
|
+
detail: text.slice(0, 800)
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
function pickConflictingStringAliases(body, ...keys) {
|
|
386
|
+
const values = keys.map((key) => {
|
|
387
|
+
const value = aliasBodyString(body[key]);
|
|
388
|
+
return value === null ? null : { key, value };
|
|
389
|
+
}).filter((entry) => entry !== null);
|
|
390
|
+
const first = values[0];
|
|
391
|
+
const conflict = values.find((entry) => entry.value !== first?.value);
|
|
392
|
+
if (first && conflict) {
|
|
393
|
+
return {
|
|
394
|
+
ok: false,
|
|
395
|
+
message: `Conflicting ${keys.join("/")} in request body`
|
|
396
|
+
};
|
|
397
|
+
}
|
|
398
|
+
return { ok: true, value: first?.value };
|
|
399
|
+
}
|
|
400
|
+
function pickConflictingNumberAliases(body, ...keys) {
|
|
401
|
+
const parseNum = (value) => {
|
|
402
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
403
|
+
return value;
|
|
404
|
+
}
|
|
405
|
+
if (typeof value === "string" && value.trim() !== "") {
|
|
406
|
+
const parsed = Number(value);
|
|
407
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
408
|
+
}
|
|
409
|
+
return void 0;
|
|
410
|
+
};
|
|
411
|
+
const values = keys.map((key) => {
|
|
412
|
+
const value = parseNum(body[key]);
|
|
413
|
+
return value === void 0 ? null : { key, value };
|
|
414
|
+
}).filter((entry) => entry !== null);
|
|
415
|
+
const first = values[0];
|
|
416
|
+
const conflict = values.find((entry) => entry.value !== first?.value);
|
|
417
|
+
if (first && conflict) {
|
|
418
|
+
return {
|
|
419
|
+
ok: false,
|
|
420
|
+
message: `Conflicting ${keys.join("/")} in request body`
|
|
421
|
+
};
|
|
422
|
+
}
|
|
423
|
+
return { ok: true, value: first?.value };
|
|
424
|
+
}
|
|
425
|
+
function pickString(obj, ...keys) {
|
|
426
|
+
for (const key of keys) {
|
|
427
|
+
const value = obj[key];
|
|
428
|
+
if (typeof value === "string" && value.trim()) {
|
|
429
|
+
return value.trim();
|
|
430
|
+
}
|
|
431
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
432
|
+
return String(Math.trunc(value));
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
return "";
|
|
436
|
+
}
|
|
437
|
+
function parseSignerUsageSnapshot(body) {
|
|
438
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
439
|
+
return null;
|
|
440
|
+
}
|
|
441
|
+
const record = body;
|
|
442
|
+
const usageRaw = record.usage;
|
|
443
|
+
if (usageRaw === null || typeof usageRaw !== "object" || Array.isArray(usageRaw)) {
|
|
444
|
+
return null;
|
|
445
|
+
}
|
|
446
|
+
const usage = usageRaw;
|
|
447
|
+
const computedFeeWei = pickString(usage, "computed_fee_wei", "computedFeeWei");
|
|
448
|
+
const usdMicrosStr = pickString(usage, "computed_fee_usd_micros", "computedFeeUsdMicros");
|
|
449
|
+
const requestId = pickString(usage, "request_id", "requestId");
|
|
450
|
+
if (!computedFeeWei || !usdMicrosStr || !requestId) {
|
|
451
|
+
return null;
|
|
452
|
+
}
|
|
453
|
+
let computedFeeUsdMicros;
|
|
454
|
+
try {
|
|
455
|
+
computedFeeUsdMicros = BigInt(usdMicrosStr);
|
|
456
|
+
} catch {
|
|
457
|
+
return null;
|
|
458
|
+
}
|
|
459
|
+
return {
|
|
460
|
+
requestId,
|
|
461
|
+
computedFeeWei,
|
|
462
|
+
computedFeeUsdMicros,
|
|
463
|
+
ethUsdPrice: pickString(usage, "eth_usd_price", "ethUsdPrice") || void 0,
|
|
464
|
+
ethUsdRoundId: pickString(usage, "eth_usd_round_id", "ethUsdRoundId") || void 0,
|
|
465
|
+
ethUsdObservedAt: pickString(usage, "eth_usd_updated_at", "ethUsdUpdatedAt", "eth_usd_observed_at") || void 0,
|
|
466
|
+
pixels: pickString(usage, "pixels") || void 0,
|
|
467
|
+
billableSecs: pickString(usage, "billable_secs", "billableSecs") || void 0,
|
|
468
|
+
pipeline: pickString(usage, "pipeline") || void 0,
|
|
469
|
+
modelId: pickString(usage, "model_id", "modelId") || void 0
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
function stripSignerUsageFromResponse(body) {
|
|
473
|
+
if (body !== null && typeof body === "object" && !Array.isArray(body)) {
|
|
474
|
+
delete body.usage;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
async function forwardToSigner(options) {
|
|
478
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
479
|
+
const baseUrl = normalizeSignerBaseUrl(options.baseUrl);
|
|
480
|
+
const url = joinSignerUrl(baseUrl, options.path);
|
|
481
|
+
const timeoutMs = options.timeoutMs ?? 3e4;
|
|
482
|
+
const controller = new AbortController();
|
|
483
|
+
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
484
|
+
const headers = { "Content-Type": "application/json" };
|
|
485
|
+
const attachJwt = options.forwardJwt ?? true;
|
|
486
|
+
if (attachJwt) {
|
|
487
|
+
const token = await getCachedDmzBearerToken(
|
|
488
|
+
options.subject,
|
|
489
|
+
"http",
|
|
490
|
+
options.getDmzToken
|
|
491
|
+
);
|
|
492
|
+
headers.Authorization = `Bearer ${token}`;
|
|
493
|
+
}
|
|
494
|
+
if (options.extraHeaders) {
|
|
495
|
+
for (const [name, value] of Object.entries(options.extraHeaders)) {
|
|
496
|
+
if (value.trim()) {
|
|
497
|
+
headers[name] = value.trim();
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
try {
|
|
502
|
+
const response = await fetchImpl(url, {
|
|
503
|
+
method: options.method,
|
|
504
|
+
headers,
|
|
505
|
+
body: options.body === void 0 ? void 0 : JSON.stringify(options.body),
|
|
506
|
+
signal: controller.signal
|
|
507
|
+
});
|
|
508
|
+
return {
|
|
509
|
+
response,
|
|
510
|
+
requestUrl: url,
|
|
511
|
+
authorizationHeader: headers.Authorization
|
|
512
|
+
};
|
|
513
|
+
} finally {
|
|
514
|
+
clearTimeout(timeout);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
function createSignerProbeContext(options) {
|
|
518
|
+
return {
|
|
519
|
+
fetchImpl: options.fetch ?? fetch,
|
|
520
|
+
signerUrl: normalizeSignerBaseUrl(options.signerUrl),
|
|
521
|
+
timeoutMs: options.timeoutMs ?? 5e3,
|
|
522
|
+
probeSubject: options.probeSubject ?? DEFAULT_PROBE_SUBJECT,
|
|
523
|
+
useJwt: options.forwardJwt ?? true,
|
|
524
|
+
getDmzToken: options.getDmzToken
|
|
525
|
+
};
|
|
526
|
+
}
|
|
527
|
+
function reachableResult(ethAddress) {
|
|
528
|
+
return { reachable: true, ethAddress };
|
|
529
|
+
}
|
|
530
|
+
async function fetchSignerStatus(ctx, headers) {
|
|
531
|
+
const response = await ctx.fetchImpl(`${ctx.signerUrl}/status`, {
|
|
532
|
+
headers,
|
|
533
|
+
signal: AbortSignal.timeout(ctx.timeoutMs)
|
|
534
|
+
});
|
|
535
|
+
if (!response.ok) {
|
|
536
|
+
return { ok: false };
|
|
537
|
+
}
|
|
538
|
+
const data = await readSignerUpstreamBody(response);
|
|
539
|
+
const ethAddress = typeof data.Address === "string" && data.Address || typeof data.address === "string" && data.address || void 0;
|
|
540
|
+
return { ok: true, ethAddress };
|
|
541
|
+
}
|
|
542
|
+
async function tryJwtStatus(ctx) {
|
|
543
|
+
if (!ctx.useJwt) {
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
try {
|
|
547
|
+
const token = await ctx.getDmzToken(ctx.probeSubject, "http");
|
|
548
|
+
const { ok, ethAddress } = await fetchSignerStatus(ctx, {
|
|
549
|
+
Authorization: `Bearer ${token}`
|
|
550
|
+
});
|
|
551
|
+
return ok ? reachableResult(ethAddress) : null;
|
|
552
|
+
} catch {
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
async function tryPlainStatus(ctx) {
|
|
557
|
+
try {
|
|
558
|
+
const { ok, ethAddress } = await fetchSignerStatus(ctx, {});
|
|
559
|
+
return ok ? reachableResult(ethAddress) : null;
|
|
560
|
+
} catch {
|
|
561
|
+
return null;
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
async function trySigningProbe(ctx) {
|
|
565
|
+
try {
|
|
566
|
+
const token = await ctx.getDmzToken(ctx.probeSubject, "http");
|
|
567
|
+
const response = await ctx.fetchImpl(`${ctx.signerUrl}/sign-orchestrator-info`, {
|
|
568
|
+
method: "POST",
|
|
569
|
+
headers: {
|
|
570
|
+
Authorization: `Bearer ${token}`,
|
|
571
|
+
"Content-Type": "application/json"
|
|
572
|
+
},
|
|
573
|
+
body: "{}",
|
|
574
|
+
signal: AbortSignal.timeout(ctx.timeoutMs)
|
|
575
|
+
});
|
|
576
|
+
return response.ok;
|
|
577
|
+
} catch {
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
async function runSignerReachabilityProbes(ctx) {
|
|
582
|
+
const jwtResult = await tryJwtStatus(ctx);
|
|
583
|
+
if (jwtResult) {
|
|
584
|
+
return jwtResult;
|
|
585
|
+
}
|
|
586
|
+
const plainResult = await tryPlainStatus(ctx);
|
|
587
|
+
if (plainResult) {
|
|
588
|
+
return plainResult;
|
|
589
|
+
}
|
|
590
|
+
if (await trySigningProbe(ctx)) {
|
|
591
|
+
return reachableResult();
|
|
592
|
+
}
|
|
593
|
+
return { reachable: false };
|
|
594
|
+
}
|
|
595
|
+
async function probeSignerHttpReachability(options) {
|
|
596
|
+
const ctx = createSignerProbeContext(options);
|
|
597
|
+
try {
|
|
598
|
+
const health = await ctx.fetchImpl(`${ctx.signerUrl}/healthz`, {
|
|
599
|
+
signal: AbortSignal.timeout(ctx.timeoutMs)
|
|
600
|
+
});
|
|
601
|
+
if (health.ok) {
|
|
602
|
+
return runSignerReachabilityProbes(ctx);
|
|
603
|
+
}
|
|
604
|
+
} catch {
|
|
605
|
+
}
|
|
606
|
+
return runSignerReachabilityProbes(ctx);
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
// src/signer/types.ts
|
|
610
|
+
function resolvesToHostedMetering(mode) {
|
|
611
|
+
return mode === "pymthouse_hosted";
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// src/signer/metering.ts
|
|
615
|
+
async function forwardWithOptionalMetering(input) {
|
|
616
|
+
const upstream = await input.forward();
|
|
617
|
+
const mode = input.config.metering?.mode;
|
|
618
|
+
if (!resolvesToHostedMetering(mode)) {
|
|
619
|
+
return upstream;
|
|
620
|
+
}
|
|
621
|
+
const body = await readSignerUpstreamBody(upstream);
|
|
622
|
+
const snapshot = upstream.ok && body !== null && typeof body === "object" ? parseSignerUsageSnapshot(body) : null;
|
|
623
|
+
if (snapshot) {
|
|
624
|
+
stripSignerUsageFromResponse(body);
|
|
625
|
+
}
|
|
626
|
+
if (upstream.ok && snapshot && snapshot.computedFeeUsdMicros > 0n) {
|
|
627
|
+
try {
|
|
628
|
+
await ingestSignedTicket({
|
|
629
|
+
issuerUrl: input.config.pymthouseIssuerUrl,
|
|
630
|
+
publicClientId: input.config.pymthouseClientId,
|
|
631
|
+
m2mClientId: input.config.pymthouseM2MClientId,
|
|
632
|
+
m2mClientSecret: input.config.pymthouseM2MClientSecret,
|
|
633
|
+
ticket: signerSnapshotToIngestPayload({
|
|
634
|
+
snapshot,
|
|
635
|
+
externalUserId: input.externalUserId
|
|
636
|
+
}),
|
|
637
|
+
fetch: input.config.fetch
|
|
638
|
+
});
|
|
639
|
+
} catch (err) {
|
|
640
|
+
console.warn("[builder-sdk] signed-ticket ingest failed:", err);
|
|
641
|
+
}
|
|
642
|
+
}
|
|
643
|
+
const headers = new Headers(upstream.headers);
|
|
644
|
+
if (!headers.has("content-type")) {
|
|
645
|
+
headers.set("Content-Type", "application/json");
|
|
646
|
+
}
|
|
647
|
+
return new Response(JSON.stringify(body), {
|
|
648
|
+
status: upstream.status,
|
|
649
|
+
statusText: upstream.statusText,
|
|
650
|
+
headers
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
654
|
+
var discoveryCache = /* @__PURE__ */ new Map();
|
|
655
|
+
function normalizedIssuerKey(issuerUrl) {
|
|
656
|
+
return stripTrailingSlashes(issuerUrl);
|
|
657
|
+
}
|
|
658
|
+
async function loadAuthorizationServer(issuerUrl, fetchImpl, options = {}) {
|
|
659
|
+
const key = normalizedIssuerKey(issuerUrl);
|
|
660
|
+
const now = Date.now();
|
|
661
|
+
const cached = discoveryCache.get(key);
|
|
662
|
+
if (!options.force && cached && now - cached.fetchedAt < CACHE_TTL_MS) {
|
|
663
|
+
return cached.as;
|
|
664
|
+
}
|
|
665
|
+
const issuerIdentifier = new URL(key);
|
|
666
|
+
const discoveryOpts = {
|
|
667
|
+
algorithm: "oidc",
|
|
668
|
+
[customFetch]: fetchImpl
|
|
669
|
+
};
|
|
670
|
+
if (options.allowInsecureHttp) {
|
|
671
|
+
discoveryOpts[allowInsecureRequests] = true;
|
|
672
|
+
}
|
|
673
|
+
let response;
|
|
674
|
+
try {
|
|
675
|
+
response = await discoveryRequest(issuerIdentifier, discoveryOpts);
|
|
676
|
+
} catch (e) {
|
|
677
|
+
throw mapDiscoveryNetworkError(e);
|
|
678
|
+
}
|
|
679
|
+
let as;
|
|
680
|
+
try {
|
|
681
|
+
as = await processDiscoveryResponse(issuerIdentifier, response);
|
|
682
|
+
} catch (e) {
|
|
683
|
+
throw mapOAuthDiscoveryError(e);
|
|
684
|
+
}
|
|
685
|
+
discoveryCache.set(key, { as, fetchedAt: now });
|
|
686
|
+
return as;
|
|
687
|
+
}
|
|
688
|
+
function mapOAuthDiscoveryError(error) {
|
|
689
|
+
if (error instanceof PmtHouseError) {
|
|
690
|
+
return error;
|
|
691
|
+
}
|
|
692
|
+
if (error instanceof Error) {
|
|
693
|
+
return new PmtHouseError(error.message, {
|
|
694
|
+
status: 500,
|
|
695
|
+
code: "oidc_discovery_invalid",
|
|
696
|
+
details: { cause: error.cause }
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
return new PmtHouseError("OIDC discovery failed", {
|
|
700
|
+
status: 500,
|
|
701
|
+
code: "oidc_discovery_invalid"
|
|
702
|
+
});
|
|
703
|
+
}
|
|
704
|
+
function mapDiscoveryNetworkError(error) {
|
|
705
|
+
if (error instanceof PmtHouseError) {
|
|
706
|
+
return error;
|
|
707
|
+
}
|
|
708
|
+
if (error instanceof Error) {
|
|
709
|
+
return new PmtHouseError(`Failed to load OIDC discovery: ${error.message}`, {
|
|
710
|
+
status: 502,
|
|
711
|
+
code: "oidc_discovery_failed"
|
|
712
|
+
});
|
|
713
|
+
}
|
|
714
|
+
return new PmtHouseError("Failed to load OIDC discovery", {
|
|
715
|
+
status: 502,
|
|
716
|
+
code: "oidc_discovery_failed"
|
|
717
|
+
});
|
|
718
|
+
}
|
|
719
|
+
|
|
720
|
+
// src/signer/fetch-json.ts
|
|
721
|
+
function oauthFailureDescription(parsed, failureLabel, status) {
|
|
722
|
+
if (typeof parsed.error_description === "string") {
|
|
723
|
+
return parsed.error_description;
|
|
724
|
+
}
|
|
725
|
+
if (typeof parsed.error === "string") {
|
|
726
|
+
return parsed.error;
|
|
727
|
+
}
|
|
728
|
+
return `${failureLabel} (${status})`;
|
|
729
|
+
}
|
|
730
|
+
async function readJsonObjectFromResponse(response, options) {
|
|
731
|
+
const text = await response.text();
|
|
732
|
+
let parsed;
|
|
733
|
+
try {
|
|
734
|
+
parsed = text ? JSON.parse(text) : {};
|
|
735
|
+
} catch {
|
|
736
|
+
throw new PmtHouseError(options.invalidJsonMessage, {
|
|
737
|
+
status: 502,
|
|
738
|
+
code: options.invalidJsonCode,
|
|
739
|
+
details: { status: response.status }
|
|
740
|
+
});
|
|
741
|
+
}
|
|
742
|
+
if (!response.ok) {
|
|
743
|
+
const description = oauthFailureDescription(parsed, options.failureLabel, response.status);
|
|
744
|
+
throw new PmtHouseError(description, {
|
|
745
|
+
status: response.status,
|
|
746
|
+
code: typeof parsed.error === "string" ? parsed.error : options.defaultErrorCode,
|
|
747
|
+
details: parsed
|
|
748
|
+
});
|
|
749
|
+
}
|
|
750
|
+
return parsed;
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
// src/signer/json-fields.ts
|
|
754
|
+
function readStringField(body, key, errorCode, messagePrefix = "Response") {
|
|
755
|
+
const value = body[key];
|
|
756
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
757
|
+
throw new PmtHouseError(`${messagePrefix} missing ${key}`, {
|
|
758
|
+
status: 502,
|
|
759
|
+
code: errorCode
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
return value.trim();
|
|
763
|
+
}
|
|
764
|
+
function readExpiresIn(body, errorCode) {
|
|
765
|
+
const expiresIn = body.expires_in;
|
|
766
|
+
if (typeof expiresIn !== "number" || !Number.isFinite(expiresIn) || expiresIn <= 0) {
|
|
767
|
+
throw new PmtHouseError("Response missing expires_in", {
|
|
768
|
+
status: 502,
|
|
769
|
+
code: errorCode
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
return Math.floor(expiresIn);
|
|
773
|
+
}
|
|
774
|
+
|
|
775
|
+
// src/signer/mint-token.ts
|
|
776
|
+
var SIGN_MINT_USER_TOKEN_SCOPE = "sign:mint_user_token";
|
|
777
|
+
var LIVEPEER_REMOTE_SIGNER_AUDIENCE = "livepeer-remote-signer";
|
|
778
|
+
var DEFAULT_TTL_REFRESH_RATIO = 0.8;
|
|
779
|
+
var TOKEN_RESPONSE_ERROR = "invalid_token_response";
|
|
780
|
+
function parseMintUserSignerTokenResponse(body, ttlRefreshRatio = DEFAULT_TTL_REFRESH_RATIO) {
|
|
781
|
+
const accessToken = readStringField(body, "access_token", TOKEN_RESPONSE_ERROR, "Token response");
|
|
782
|
+
const expiresIn = readExpiresIn(body, TOKEN_RESPONSE_ERROR);
|
|
783
|
+
const balanceUsdMicros = readStringField(
|
|
784
|
+
body,
|
|
785
|
+
"balanceUsdMicros",
|
|
786
|
+
TOKEN_RESPONSE_ERROR,
|
|
787
|
+
"Token response"
|
|
788
|
+
);
|
|
789
|
+
const lifetimeGrantedUsdMicros = readStringField(
|
|
790
|
+
body,
|
|
791
|
+
"lifetimeGrantedUsdMicros",
|
|
792
|
+
TOKEN_RESPONSE_ERROR,
|
|
793
|
+
"Token response"
|
|
794
|
+
);
|
|
795
|
+
const now = Date.now();
|
|
796
|
+
const expiresAt = now + expiresIn * 1e3;
|
|
797
|
+
const refreshAt = now + Math.floor(expiresIn * 1e3 * ttlRefreshRatio);
|
|
798
|
+
return {
|
|
799
|
+
jwt: accessToken,
|
|
800
|
+
expiresAt,
|
|
801
|
+
refreshAt,
|
|
802
|
+
balanceUsdMicros,
|
|
803
|
+
lifetimeGrantedUsdMicros
|
|
804
|
+
};
|
|
805
|
+
}
|
|
806
|
+
async function mintUserSignerToken(options) {
|
|
807
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
808
|
+
const issuerUrl = stripTrailingSlashes(options.issuerUrl);
|
|
809
|
+
const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {
|
|
810
|
+
allowInsecureHttp: options.allowInsecureHttp
|
|
811
|
+
});
|
|
812
|
+
const tokenEndpoint = as.token_endpoint;
|
|
813
|
+
if (!tokenEndpoint) {
|
|
814
|
+
throw new PmtHouseError("OIDC discovery document is missing token_endpoint", {
|
|
815
|
+
status: 500,
|
|
816
|
+
code: "oidc_discovery_invalid"
|
|
817
|
+
});
|
|
818
|
+
}
|
|
819
|
+
const body = new URLSearchParams({
|
|
820
|
+
grant_type: "client_credentials",
|
|
821
|
+
scope: SIGN_MINT_USER_TOKEN_SCOPE,
|
|
822
|
+
external_user_id: options.externalUserId,
|
|
823
|
+
audience: LIVEPEER_REMOTE_SIGNER_AUDIENCE
|
|
824
|
+
});
|
|
825
|
+
const response = await fetchImpl(tokenEndpoint, {
|
|
826
|
+
method: "POST",
|
|
827
|
+
headers: {
|
|
828
|
+
Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),
|
|
829
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
830
|
+
Accept: "application/json"
|
|
831
|
+
},
|
|
832
|
+
body: body.toString(),
|
|
833
|
+
cache: "no-store"
|
|
834
|
+
});
|
|
835
|
+
const parsed = await readJsonObjectFromResponse(response, {
|
|
836
|
+
invalidJsonMessage: "Token endpoint returned invalid JSON",
|
|
837
|
+
invalidJsonCode: TOKEN_RESPONSE_ERROR,
|
|
838
|
+
failureLabel: "Token mint failed",
|
|
839
|
+
defaultErrorCode: "token_mint_failed"
|
|
840
|
+
});
|
|
841
|
+
return parseMintUserSignerTokenResponse(parsed);
|
|
842
|
+
}
|
|
843
|
+
|
|
844
|
+
// src/signer/device-exchange.ts
|
|
845
|
+
var TOKEN_EXCHANGE_GRANT = "urn:ietf:params:oauth:grant-type:token-exchange";
|
|
846
|
+
var SUBJECT_ACCESS_TOKEN_TYPE = "urn:ietf:params:oauth:token-type:access_token";
|
|
847
|
+
var EXCHANGE_RESPONSE_ERROR = "invalid_exchange_response";
|
|
848
|
+
function extractSignerAccessTokenFromExchangeBody(body) {
|
|
849
|
+
const tokenObj = body.token;
|
|
850
|
+
if (tokenObj !== null && typeof tokenObj === "object" && !Array.isArray(tokenObj)) {
|
|
851
|
+
const nested = tokenObj;
|
|
852
|
+
for (const key of ["accessToken", "access_token"]) {
|
|
853
|
+
const value = nested[key];
|
|
854
|
+
if (typeof value === "string" && value.trim()) {
|
|
855
|
+
return value.trim();
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
}
|
|
859
|
+
for (const key of ["accessToken", "access_token"]) {
|
|
860
|
+
const value = body[key];
|
|
861
|
+
if (typeof value === "string" && value.trim()) {
|
|
862
|
+
return value.trim();
|
|
863
|
+
}
|
|
864
|
+
}
|
|
865
|
+
throw new PmtHouseError("Device exchange response missing signer access token", {
|
|
866
|
+
status: 502,
|
|
867
|
+
code: "invalid_exchange_response"
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
function normalizeDeviceExchangeResponse(minted, options) {
|
|
871
|
+
const scope = minted.scope.trim() || "sign:job";
|
|
872
|
+
const body = {
|
|
873
|
+
access_token: minted.access_token,
|
|
874
|
+
token_type: "Bearer",
|
|
875
|
+
expires_in: minted.expires_in,
|
|
876
|
+
scope,
|
|
877
|
+
balanceUsdMicros: minted.balanceUsdMicros,
|
|
878
|
+
lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros,
|
|
879
|
+
token: {
|
|
880
|
+
accessToken: minted.access_token,
|
|
881
|
+
access_token: minted.access_token,
|
|
882
|
+
expiresIn: minted.expires_in,
|
|
883
|
+
expires_in: minted.expires_in,
|
|
884
|
+
scope,
|
|
885
|
+
balanceUsdMicros: minted.balanceUsdMicros,
|
|
886
|
+
lifetimeGrantedUsdMicros: minted.lifetimeGrantedUsdMicros
|
|
887
|
+
}
|
|
888
|
+
};
|
|
889
|
+
const signerUrl = options?.signerUrl?.trim();
|
|
890
|
+
if (signerUrl) {
|
|
891
|
+
body.signerUrl = signerUrl;
|
|
892
|
+
}
|
|
893
|
+
return body;
|
|
894
|
+
}
|
|
895
|
+
async function parseDeviceExchangeRequestBody(request) {
|
|
896
|
+
let body;
|
|
897
|
+
try {
|
|
898
|
+
body = await request.json();
|
|
899
|
+
} catch {
|
|
900
|
+
throw new PmtHouseError("Request body must be JSON", {
|
|
901
|
+
status: 400,
|
|
902
|
+
code: "invalid_request"
|
|
903
|
+
});
|
|
904
|
+
}
|
|
905
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
906
|
+
throw new PmtHouseError("Request body must be a JSON object", {
|
|
907
|
+
status: 400,
|
|
908
|
+
code: "invalid_request"
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
const record = body;
|
|
912
|
+
const deviceTokenRaw = record.deviceToken;
|
|
913
|
+
if (typeof deviceTokenRaw !== "string" || !deviceTokenRaw.trim()) {
|
|
914
|
+
throw new PmtHouseError("Request body must include deviceToken", {
|
|
915
|
+
status: 400,
|
|
916
|
+
code: "invalid_request"
|
|
917
|
+
});
|
|
918
|
+
}
|
|
919
|
+
const deviceToken = deviceTokenRaw.trim();
|
|
920
|
+
const scope = typeof record.scope === "string" && record.scope.trim() ? record.scope.trim() : void 0;
|
|
921
|
+
const clientId = typeof record.clientId === "string" && record.clientId.trim() ? record.clientId.trim() : void 0;
|
|
922
|
+
return { deviceToken, scope, clientId };
|
|
923
|
+
}
|
|
924
|
+
async function mintSignerTokenFromDeviceToken(options) {
|
|
925
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
926
|
+
const issuerUrl = stripTrailingSlashes(options.issuerUrl);
|
|
927
|
+
const as = await loadAuthorizationServer(issuerUrl, fetchImpl, {
|
|
928
|
+
allowInsecureHttp: options.allowInsecureHttp
|
|
929
|
+
});
|
|
930
|
+
const tokenEndpoint = as.token_endpoint;
|
|
931
|
+
if (!tokenEndpoint) {
|
|
932
|
+
throw new PmtHouseError("OIDC discovery document is missing token_endpoint", {
|
|
933
|
+
status: 500,
|
|
934
|
+
code: "oidc_discovery_invalid"
|
|
935
|
+
});
|
|
936
|
+
}
|
|
937
|
+
const audience = options.audience?.trim() || LIVEPEER_REMOTE_SIGNER_AUDIENCE;
|
|
938
|
+
const params = new URLSearchParams({
|
|
939
|
+
grant_type: TOKEN_EXCHANGE_GRANT,
|
|
940
|
+
subject_token: options.deviceToken,
|
|
941
|
+
subject_token_type: SUBJECT_ACCESS_TOKEN_TYPE,
|
|
942
|
+
audience,
|
|
943
|
+
resource: audience
|
|
944
|
+
});
|
|
945
|
+
if (options.scope?.trim()) {
|
|
946
|
+
params.set("scope", options.scope.trim());
|
|
947
|
+
}
|
|
948
|
+
const response = await fetchImpl(tokenEndpoint, {
|
|
949
|
+
method: "POST",
|
|
950
|
+
headers: {
|
|
951
|
+
Authorization: encodeClientSecretBasic(options.m2mClientId, options.m2mClientSecret),
|
|
952
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
953
|
+
Accept: "application/json"
|
|
954
|
+
},
|
|
955
|
+
body: params.toString(),
|
|
956
|
+
cache: "no-store"
|
|
957
|
+
});
|
|
958
|
+
const parsed = await readJsonObjectFromResponse(response, {
|
|
959
|
+
invalidJsonMessage: "Token endpoint returned invalid JSON",
|
|
960
|
+
invalidJsonCode: "invalid_token_response",
|
|
961
|
+
failureLabel: "Signer JWT exchange failed",
|
|
962
|
+
defaultErrorCode: "token_exchange_failed"
|
|
963
|
+
});
|
|
964
|
+
const cached = parseMintUserSignerTokenResponse(parsed);
|
|
965
|
+
return {
|
|
966
|
+
access_token: cached.jwt,
|
|
967
|
+
expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),
|
|
968
|
+
scope: readStringField(parsed, "scope", EXCHANGE_RESPONSE_ERROR),
|
|
969
|
+
balanceUsdMicros: cached.balanceUsdMicros,
|
|
970
|
+
lifetimeGrantedUsdMicros: cached.lifetimeGrantedUsdMicros
|
|
971
|
+
};
|
|
972
|
+
}
|
|
973
|
+
async function exchangeDeviceTokenForSigner(options) {
|
|
974
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
975
|
+
const url = `${stripTrailingSlashes(options.facadeUrl)}/api/signer/device/exchange`;
|
|
976
|
+
const body = { deviceToken: options.deviceToken };
|
|
977
|
+
if (options.scope?.trim()) {
|
|
978
|
+
body.scope = options.scope.trim();
|
|
979
|
+
}
|
|
980
|
+
if (options.clientId?.trim()) {
|
|
981
|
+
body.clientId = options.clientId.trim();
|
|
982
|
+
}
|
|
983
|
+
const response = await fetchImpl(url, {
|
|
984
|
+
method: "POST",
|
|
985
|
+
headers: {
|
|
986
|
+
"Content-Type": "application/json",
|
|
987
|
+
Accept: "application/json"
|
|
988
|
+
},
|
|
989
|
+
body: JSON.stringify(body),
|
|
990
|
+
cache: "no-store"
|
|
991
|
+
});
|
|
992
|
+
const parsed = await readJsonObjectFromResponse(response, {
|
|
993
|
+
invalidJsonMessage: "Device exchange returned invalid JSON",
|
|
994
|
+
invalidJsonCode: EXCHANGE_RESPONSE_ERROR,
|
|
995
|
+
failureLabel: "Device exchange failed",
|
|
996
|
+
defaultErrorCode: "device_exchange_failed"
|
|
997
|
+
});
|
|
998
|
+
const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);
|
|
999
|
+
const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;
|
|
1000
|
+
const signerUrl = typeof signerUrlRaw === "string" && signerUrlRaw.trim() ? signerUrlRaw.trim() : void 0;
|
|
1001
|
+
return normalizeDeviceExchangeResponse(
|
|
1002
|
+
{
|
|
1003
|
+
access_token: accessToken,
|
|
1004
|
+
expires_in: readExpiresIn(parsed, EXCHANGE_RESPONSE_ERROR),
|
|
1005
|
+
scope: typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : "sign:job",
|
|
1006
|
+
balanceUsdMicros: typeof parsed.balanceUsdMicros === "string" ? parsed.balanceUsdMicros : "0",
|
|
1007
|
+
lifetimeGrantedUsdMicros: typeof parsed.lifetimeGrantedUsdMicros === "string" ? parsed.lifetimeGrantedUsdMicros : "0"
|
|
1008
|
+
},
|
|
1009
|
+
{ signerUrl }
|
|
1010
|
+
);
|
|
1011
|
+
}
|
|
1012
|
+
function resolveMint(config) {
|
|
1013
|
+
if ("mint" in config && typeof config.mint === "function") {
|
|
1014
|
+
return config.mint;
|
|
1015
|
+
}
|
|
1016
|
+
return (deviceToken, context) => mintSignerTokenFromDeviceToken({
|
|
1017
|
+
issuerUrl: config.issuerUrl,
|
|
1018
|
+
m2mClientId: config.m2mClientId,
|
|
1019
|
+
m2mClientSecret: config.m2mClientSecret,
|
|
1020
|
+
deviceToken,
|
|
1021
|
+
scope: context.scope,
|
|
1022
|
+
audience: config.audience,
|
|
1023
|
+
fetch: config.fetch,
|
|
1024
|
+
allowInsecureHttp: config.allowInsecureHttp
|
|
1025
|
+
});
|
|
1026
|
+
}
|
|
1027
|
+
function resolveSignerUrlFromConfig(config) {
|
|
1028
|
+
if ("signerUrl" in config && typeof config.signerUrl === "string" && config.signerUrl.trim()) {
|
|
1029
|
+
return config.signerUrl.trim();
|
|
1030
|
+
}
|
|
1031
|
+
if ("getSignerUrl" in config && typeof config.getSignerUrl === "function") {
|
|
1032
|
+
return config.getSignerUrl();
|
|
1033
|
+
}
|
|
1034
|
+
return void 0;
|
|
1035
|
+
}
|
|
1036
|
+
function createDeviceExchangeHandler(config) {
|
|
1037
|
+
const mint = resolveMint(config);
|
|
1038
|
+
return async function deviceExchangeHandler(request) {
|
|
1039
|
+
try {
|
|
1040
|
+
if (request.method !== "POST") {
|
|
1041
|
+
return new Response(JSON.stringify({ error: "method_not_allowed" }), {
|
|
1042
|
+
status: 405,
|
|
1043
|
+
headers: { "Content-Type": "application/json" }
|
|
1044
|
+
});
|
|
1045
|
+
}
|
|
1046
|
+
const parsed = await parseDeviceExchangeRequestBody(request);
|
|
1047
|
+
const minted = await mint(parsed.deviceToken, {
|
|
1048
|
+
scope: parsed.scope,
|
|
1049
|
+
clientId: parsed.clientId
|
|
1050
|
+
});
|
|
1051
|
+
const signerUrlValue = await resolveSignerUrlFromConfig(config);
|
|
1052
|
+
const body = normalizeDeviceExchangeResponse(minted, {
|
|
1053
|
+
signerUrl: typeof signerUrlValue === "string" ? signerUrlValue : void 0
|
|
1054
|
+
});
|
|
1055
|
+
return new Response(JSON.stringify(body), {
|
|
1056
|
+
status: 200,
|
|
1057
|
+
headers: {
|
|
1058
|
+
"Content-Type": "application/json",
|
|
1059
|
+
"Cache-Control": "no-store"
|
|
1060
|
+
}
|
|
1061
|
+
});
|
|
1062
|
+
} catch (error) {
|
|
1063
|
+
return signerHandlerErrorResponse(error);
|
|
1064
|
+
}
|
|
1065
|
+
};
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
// src/signer/api-key-exchange.ts
|
|
1069
|
+
var EXCHANGE_RESPONSE_ERROR2 = "invalid_exchange_response";
|
|
1070
|
+
async function parseApiKeyExchangeRequestBody(request) {
|
|
1071
|
+
let body;
|
|
1072
|
+
try {
|
|
1073
|
+
body = await request.json();
|
|
1074
|
+
} catch {
|
|
1075
|
+
throw new PmtHouseError("Request body must be JSON", {
|
|
1076
|
+
status: 400,
|
|
1077
|
+
code: "invalid_request"
|
|
1078
|
+
});
|
|
1079
|
+
}
|
|
1080
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
1081
|
+
throw new PmtHouseError("Request body must be a JSON object", {
|
|
1082
|
+
status: 400,
|
|
1083
|
+
code: "invalid_request"
|
|
1084
|
+
});
|
|
1085
|
+
}
|
|
1086
|
+
const record = body;
|
|
1087
|
+
const apiKeyRaw = record.apiKey;
|
|
1088
|
+
if (typeof apiKeyRaw !== "string" || !apiKeyRaw.trim()) {
|
|
1089
|
+
throw new PmtHouseError("Request body must include apiKey", {
|
|
1090
|
+
status: 400,
|
|
1091
|
+
code: "invalid_request"
|
|
1092
|
+
});
|
|
1093
|
+
}
|
|
1094
|
+
const scope = typeof record.scope === "string" && record.scope.trim() ? record.scope.trim() : void 0;
|
|
1095
|
+
const clientId = typeof record.clientId === "string" && record.clientId.trim() ? record.clientId.trim() : void 0;
|
|
1096
|
+
return { apiKey: apiKeyRaw.trim(), scope, clientId };
|
|
1097
|
+
}
|
|
1098
|
+
async function mintUserAccessTokenFromApiKey(input) {
|
|
1099
|
+
const fetchImpl = input.fetch ?? fetch;
|
|
1100
|
+
const issuerOrigin = stripIssuerOriginFromOidcUrl(input.issuerUrl);
|
|
1101
|
+
const url = `${issuerOrigin}/api/v1/apps/${encodeURIComponent(input.publicClientId)}/auth/api-key/token`;
|
|
1102
|
+
const response = await fetchImpl(url, {
|
|
1103
|
+
method: "POST",
|
|
1104
|
+
headers: {
|
|
1105
|
+
Authorization: `Bearer ${input.apiKey}`,
|
|
1106
|
+
"Content-Type": "application/json",
|
|
1107
|
+
Accept: "application/json"
|
|
1108
|
+
},
|
|
1109
|
+
body: JSON.stringify(input.scope ? { scope: input.scope } : {}),
|
|
1110
|
+
cache: "no-store"
|
|
1111
|
+
});
|
|
1112
|
+
const parsed = await readJsonObjectFromResponse(response, {
|
|
1113
|
+
invalidJsonMessage: "API key token exchange returned invalid JSON",
|
|
1114
|
+
invalidJsonCode: "invalid_token_response",
|
|
1115
|
+
failureLabel: "API key token exchange failed",
|
|
1116
|
+
defaultErrorCode: "api_key_token_exchange_failed"
|
|
1117
|
+
});
|
|
1118
|
+
const accessToken = parsed.access_token;
|
|
1119
|
+
if (typeof accessToken !== "string" || !accessToken.trim()) {
|
|
1120
|
+
throw new PmtHouseError("API key token exchange missing access_token", {
|
|
1121
|
+
status: 502,
|
|
1122
|
+
code: EXCHANGE_RESPONSE_ERROR2
|
|
1123
|
+
});
|
|
1124
|
+
}
|
|
1125
|
+
const expiresIn = typeof parsed.expires_in === "number" && Number.isFinite(parsed.expires_in) ? parsed.expires_in : 900;
|
|
1126
|
+
const scope = typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : input.scope?.trim() || "sign:job";
|
|
1127
|
+
return {
|
|
1128
|
+
access_token: accessToken.trim(),
|
|
1129
|
+
expires_in: expiresIn,
|
|
1130
|
+
scope
|
|
1131
|
+
};
|
|
1132
|
+
}
|
|
1133
|
+
async function mintSignerSessionFromApiKey(input) {
|
|
1134
|
+
const userToken = await mintUserAccessTokenFromApiKey({
|
|
1135
|
+
issuerUrl: input.issuerUrl,
|
|
1136
|
+
publicClientId: input.publicClientId,
|
|
1137
|
+
apiKey: input.apiKey,
|
|
1138
|
+
scope: input.scope,
|
|
1139
|
+
fetch: input.fetch
|
|
1140
|
+
});
|
|
1141
|
+
return mintSignerTokenFromDeviceToken({
|
|
1142
|
+
issuerUrl: input.issuerUrl,
|
|
1143
|
+
m2mClientId: input.m2mClientId,
|
|
1144
|
+
m2mClientSecret: input.m2mClientSecret,
|
|
1145
|
+
deviceToken: userToken.access_token,
|
|
1146
|
+
scope: userToken.scope,
|
|
1147
|
+
audience: input.audience,
|
|
1148
|
+
fetch: input.fetch,
|
|
1149
|
+
allowInsecureHttp: input.allowInsecureHttp
|
|
1150
|
+
});
|
|
1151
|
+
}
|
|
1152
|
+
async function exchangeApiKeyForSigner(options) {
|
|
1153
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
1154
|
+
const url = `${stripTrailingSlashes(options.facadeUrl)}/api/pymthouse/keys/exchange`;
|
|
1155
|
+
const body = { apiKey: options.apiKey };
|
|
1156
|
+
if (options.scope?.trim()) {
|
|
1157
|
+
body.scope = options.scope.trim();
|
|
1158
|
+
}
|
|
1159
|
+
if (options.clientId?.trim()) {
|
|
1160
|
+
body.clientId = options.clientId.trim();
|
|
1161
|
+
}
|
|
1162
|
+
const response = await fetchImpl(url, {
|
|
1163
|
+
method: "POST",
|
|
1164
|
+
headers: {
|
|
1165
|
+
"Content-Type": "application/json",
|
|
1166
|
+
Accept: "application/json"
|
|
1167
|
+
},
|
|
1168
|
+
body: JSON.stringify(body),
|
|
1169
|
+
cache: "no-store"
|
|
1170
|
+
});
|
|
1171
|
+
const parsed = await readJsonObjectFromResponse(response, {
|
|
1172
|
+
invalidJsonMessage: "API key exchange returned invalid JSON",
|
|
1173
|
+
invalidJsonCode: EXCHANGE_RESPONSE_ERROR2,
|
|
1174
|
+
failureLabel: "API key exchange failed",
|
|
1175
|
+
defaultErrorCode: "api_key_exchange_failed"
|
|
1176
|
+
});
|
|
1177
|
+
const accessToken = extractSignerAccessTokenFromExchangeBody(parsed);
|
|
1178
|
+
const signerUrlRaw = parsed.signerUrl ?? parsed.signer_url;
|
|
1179
|
+
const signerUrl = typeof signerUrlRaw === "string" && signerUrlRaw.trim() ? signerUrlRaw.trim() : void 0;
|
|
1180
|
+
return normalizeDeviceExchangeResponse(
|
|
1181
|
+
{
|
|
1182
|
+
access_token: accessToken,
|
|
1183
|
+
expires_in: typeof parsed.expires_in === "number" && Number.isFinite(parsed.expires_in) ? parsed.expires_in : 3600,
|
|
1184
|
+
scope: typeof parsed.scope === "string" && parsed.scope.trim() ? parsed.scope.trim() : "sign:job",
|
|
1185
|
+
balanceUsdMicros: typeof parsed.balanceUsdMicros === "string" ? parsed.balanceUsdMicros : "0",
|
|
1186
|
+
lifetimeGrantedUsdMicros: typeof parsed.lifetimeGrantedUsdMicros === "string" ? parsed.lifetimeGrantedUsdMicros : "0"
|
|
1187
|
+
},
|
|
1188
|
+
{ signerUrl }
|
|
1189
|
+
);
|
|
1190
|
+
}
|
|
1191
|
+
function createApiKeyExchangeHandler(config) {
|
|
1192
|
+
const publicClientId = config.publicClientId.trim();
|
|
1193
|
+
return async function apiKeyExchangeHandler(request) {
|
|
1194
|
+
try {
|
|
1195
|
+
if (request.method !== "POST") {
|
|
1196
|
+
return new Response(JSON.stringify({ error: "method_not_allowed" }), {
|
|
1197
|
+
status: 405,
|
|
1198
|
+
headers: { "Content-Type": "application/json" }
|
|
1199
|
+
});
|
|
1200
|
+
}
|
|
1201
|
+
const parsed = await parseApiKeyExchangeRequestBody(request);
|
|
1202
|
+
const effectiveClientId = parsed.clientId?.trim() || publicClientId;
|
|
1203
|
+
if (effectiveClientId !== publicClientId) {
|
|
1204
|
+
throw new PmtHouseError("clientId does not match configured public client", {
|
|
1205
|
+
status: 400,
|
|
1206
|
+
code: "invalid_request"
|
|
1207
|
+
});
|
|
1208
|
+
}
|
|
1209
|
+
const minted = await mintSignerSessionFromApiKey({
|
|
1210
|
+
issuerUrl: config.issuerUrl,
|
|
1211
|
+
publicClientId,
|
|
1212
|
+
m2mClientId: config.m2mClientId,
|
|
1213
|
+
m2mClientSecret: config.m2mClientSecret,
|
|
1214
|
+
apiKey: parsed.apiKey,
|
|
1215
|
+
scope: parsed.scope,
|
|
1216
|
+
audience: config.audience,
|
|
1217
|
+
fetch: config.fetch,
|
|
1218
|
+
allowInsecureHttp: config.allowInsecureHttp
|
|
1219
|
+
});
|
|
1220
|
+
const signerUrlValue = typeof config.signerUrl === "string" && config.signerUrl.trim() ? config.signerUrl.trim() : void 0;
|
|
1221
|
+
const body = normalizeDeviceExchangeResponse(minted, { signerUrl: signerUrlValue });
|
|
1222
|
+
return new Response(JSON.stringify(body), {
|
|
1223
|
+
status: 200,
|
|
1224
|
+
headers: {
|
|
1225
|
+
"Content-Type": "application/json",
|
|
1226
|
+
"Cache-Control": "no-store"
|
|
1227
|
+
}
|
|
1228
|
+
});
|
|
1229
|
+
} catch (error) {
|
|
1230
|
+
return signerHandlerErrorResponse(error);
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// src/signer/server.ts
|
|
1236
|
+
function unauthorizedResponse() {
|
|
1237
|
+
return new Response(JSON.stringify({ error: "unauthorized" }), {
|
|
1238
|
+
status: 401,
|
|
1239
|
+
headers: { "Content-Type": "application/json" }
|
|
1240
|
+
});
|
|
1241
|
+
}
|
|
1242
|
+
function toResponse(result) {
|
|
1243
|
+
if (result instanceof Response) {
|
|
1244
|
+
return result;
|
|
1245
|
+
}
|
|
1246
|
+
return new Response(result.body === void 0 ? null : JSON.stringify(result.body), {
|
|
1247
|
+
status: result.status,
|
|
1248
|
+
headers: { "Content-Type": "application/json" }
|
|
1249
|
+
});
|
|
1250
|
+
}
|
|
1251
|
+
function createDirectSignerProxyHandler(config) {
|
|
1252
|
+
const tokenManager = createSignerTokenManager({
|
|
1253
|
+
publicClientId: config.pymthouseClientId,
|
|
1254
|
+
mint: (externalUserId) => mintUserSignerToken({
|
|
1255
|
+
issuerUrl: config.pymthouseIssuerUrl,
|
|
1256
|
+
m2mClientId: config.pymthouseM2MClientId,
|
|
1257
|
+
m2mClientSecret: config.pymthouseM2MClientSecret,
|
|
1258
|
+
externalUserId,
|
|
1259
|
+
fetch: config.fetch,
|
|
1260
|
+
allowInsecureHttp: config.allowInsecureHttp
|
|
1261
|
+
})
|
|
1262
|
+
});
|
|
1263
|
+
async function runBeforeSign(token, externalUserId, request) {
|
|
1264
|
+
if (!config.beforeSign) {
|
|
1265
|
+
return null;
|
|
1266
|
+
}
|
|
1267
|
+
const result = await config.beforeSign({ token, externalUserId, request });
|
|
1268
|
+
if (!result) {
|
|
1269
|
+
return null;
|
|
1270
|
+
}
|
|
1271
|
+
return toResponse(result);
|
|
1272
|
+
}
|
|
1273
|
+
async function forwardOnce(token, request) {
|
|
1274
|
+
return forwardDirectSignerRequest({
|
|
1275
|
+
request,
|
|
1276
|
+
remoteSignerUrl: config.remoteSignerUrl,
|
|
1277
|
+
jwt: token.jwt,
|
|
1278
|
+
proxyPathPrefix: config.proxyPathPrefix,
|
|
1279
|
+
defaultRemotePath: config.defaultRemotePath,
|
|
1280
|
+
fetch: config.fetch
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
const handler = async function directSignerProxyHandler(request) {
|
|
1284
|
+
try {
|
|
1285
|
+
const session = await config.authenticate(request);
|
|
1286
|
+
if (session == null) {
|
|
1287
|
+
return unauthorizedResponse();
|
|
1288
|
+
}
|
|
1289
|
+
const externalUserId = (await config.resolveExternalUserId(session)).trim();
|
|
1290
|
+
if (!externalUserId) {
|
|
1291
|
+
throw new PmtHouseError("resolveExternalUserId returned an empty id", {
|
|
1292
|
+
status: 500,
|
|
1293
|
+
code: "invalid_external_user_id"
|
|
1294
|
+
});
|
|
1295
|
+
}
|
|
1296
|
+
let token = await tokenManager.getToken(externalUserId);
|
|
1297
|
+
const blocked = await runBeforeSign(token, externalUserId, request);
|
|
1298
|
+
if (blocked) {
|
|
1299
|
+
return blocked;
|
|
1300
|
+
}
|
|
1301
|
+
let upstream = await forwardWithOptionalMetering({
|
|
1302
|
+
config,
|
|
1303
|
+
externalUserId,
|
|
1304
|
+
forward: () => forwardOnce(token, request)
|
|
1305
|
+
});
|
|
1306
|
+
if (upstream.status === 401) {
|
|
1307
|
+
tokenManager.invalidate(externalUserId);
|
|
1308
|
+
token = await tokenManager.getToken(externalUserId, { forceRefresh: true });
|
|
1309
|
+
const retryBlocked = await runBeforeSign(token, externalUserId, request);
|
|
1310
|
+
if (retryBlocked) {
|
|
1311
|
+
return retryBlocked;
|
|
1312
|
+
}
|
|
1313
|
+
upstream = await forwardWithOptionalMetering({
|
|
1314
|
+
config,
|
|
1315
|
+
externalUserId,
|
|
1316
|
+
forward: () => forwardOnce(token, request)
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
return upstream;
|
|
1320
|
+
} catch (error) {
|
|
1321
|
+
return signerHandlerErrorResponse(error);
|
|
1322
|
+
}
|
|
1323
|
+
};
|
|
1324
|
+
handler.getCachedUsage = (externalUserId) => tokenManager.peek(externalUserId);
|
|
1325
|
+
handler.invalidateToken = (externalUserId) => tokenManager.invalidate(externalUserId);
|
|
1326
|
+
return handler;
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
export { LIVEPEER_REMOTE_SIGNER_AUDIENCE, SIGN_MINT_USER_TOKEN_SCOPE, createApiKeyExchangeHandler, createDeviceExchangeHandler, createDirectSignerProxyHandler, createSignerTokenManager, decodeJwtPayload, exchangeApiKeyForSigner, exchangeDeviceTokenForSigner, extractSignerAccessTokenFromExchangeBody, forwardDirectSignerRequest, forwardToSigner, forwardWithOptionalMetering, getCachedDmzBearerToken, identityFromJwtPayload, ingestSignedTicket, livepeerIdentityHeaders, mintSignerSessionFromApiKey, mintSignerTokenFromDeviceToken, mintUserAccessTokenFromApiKey, mintUserSignerToken, normalizeDeviceExchangeResponse, normalizeSignerBaseUrl, parseApiKeyExchangeRequestBody, parseDeviceExchangeRequestBody, parseMintUserSignerTokenResponse, parseSignerUsageSnapshot, pickConflictingNumberAliases, pickConflictingStringAliases, probeSignerHttpReachability, readSignerUpstreamBody, resolveSignerBaseUrl, signerSnapshotToIngestPayload, stripSignerUsageFromResponse };
|
|
1330
|
+
//# sourceMappingURL=server.js.map
|
|
1331
|
+
//# sourceMappingURL=server.js.map
|