@pymthouse/builder-sdk 0.3.1 → 0.4.1-rc.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 +54 -28
- package/dist/{client-BHfjDvIe.d.ts → client-CauCfGa7.d.ts} +1 -1
- package/dist/{client-CvhJEhjV.d.cts → client-D1Xz-xlx.d.cts} +1 -1
- package/dist/config.cjs +0 -21
- package/dist/config.cjs.map +1 -1
- package/dist/config.d.cts +1 -5
- package/dist/config.d.ts +1 -5
- package/dist/config.js +1 -20
- package/dist/config.js.map +1 -1
- package/dist/device-initiate.cjs.map +1 -1
- package/dist/device-initiate.js.map +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.map +1 -1
- package/dist/env.cjs +13 -4
- 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 +13 -4
- package/dist/env.js.map +1 -1
- package/dist/index-BTDKEorK.d.ts +64 -0
- package/dist/index-BixH4VIG.d.cts +64 -0
- package/dist/index.cjs +13 -4
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +29 -5
- package/dist/index.d.ts +29 -5
- package/dist/index.js +13 -4
- package/dist/index.js.map +1 -1
- package/dist/{ingest-DoKJTWU9.d.ts → proxy-JrT6raU_.d.cts} +5 -42
- package/dist/{ingest-B3Yi8Tb1.d.cts → proxy-U32DFNuj.d.ts} +5 -42
- package/dist/signer/server.cjs +799 -895
- package/dist/signer/server.cjs.map +1 -1
- package/dist/signer/server.d.cts +9 -13
- package/dist/signer/server.d.ts +9 -13
- package/dist/signer/server.js +799 -893
- package/dist/signer/server.js.map +1 -1
- package/dist/signer/webhook/adapters/api-key.cjs +78 -0
- package/dist/signer/webhook/adapters/api-key.cjs.map +1 -0
- package/dist/signer/webhook/adapters/api-key.d.cts +18 -0
- package/dist/signer/webhook/adapters/api-key.d.ts +18 -0
- package/dist/signer/webhook/adapters/api-key.js +76 -0
- package/dist/signer/webhook/adapters/api-key.js.map +1 -0
- package/dist/signer/webhook/adapters/composite.cjs +60 -0
- package/dist/signer/webhook/adapters/composite.cjs.map +1 -0
- package/dist/signer/webhook/adapters/composite.d.cts +5 -0
- package/dist/signer/webhook/adapters/composite.d.ts +5 -0
- package/dist/signer/webhook/adapters/composite.js +58 -0
- package/dist/signer/webhook/adapters/composite.js.map +1 -0
- package/dist/signer/webhook/adapters/oauth1.cjs +18 -0
- package/dist/signer/webhook/adapters/oauth1.cjs.map +1 -0
- package/dist/signer/webhook/adapters/oauth1.d.cts +19 -0
- package/dist/signer/webhook/adapters/oauth1.d.ts +19 -0
- package/dist/signer/webhook/adapters/oauth1.js +16 -0
- package/dist/signer/webhook/adapters/oauth1.js.map +1 -0
- package/dist/signer/webhook/adapters/oidc.cjs +522 -0
- package/dist/signer/webhook/adapters/oidc.cjs.map +1 -0
- package/dist/signer/webhook/adapters/oidc.d.cts +4 -0
- package/dist/signer/webhook/adapters/oidc.d.ts +4 -0
- package/dist/signer/webhook/adapters/oidc.js +515 -0
- package/dist/signer/webhook/adapters/oidc.js.map +1 -0
- package/dist/signer/webhook/adapters/trusted-headers.cjs +103 -0
- package/dist/signer/webhook/adapters/trusted-headers.cjs.map +1 -0
- package/dist/signer/webhook/adapters/trusted-headers.d.cts +18 -0
- package/dist/signer/webhook/adapters/trusted-headers.d.ts +18 -0
- package/dist/signer/webhook/adapters/trusted-headers.js +99 -0
- package/dist/signer/webhook/adapters/trusted-headers.js.map +1 -0
- package/dist/signer/webhook.cjs +747 -0
- package/dist/signer/webhook.cjs.map +1 -0
- package/dist/signer/webhook.d.cts +26 -0
- package/dist/signer/webhook.d.ts +26 -0
- package/dist/signer/webhook.js +721 -0
- package/dist/signer/webhook.js.map +1 -0
- package/dist/tokens.d.cts +1 -1
- package/dist/tokens.d.ts +1 -1
- package/dist/{types-_R1AwEZp.d.cts → types-BORaHW_x.d.cts} +5 -5
- package/dist/{types-_R1AwEZp.d.ts → types-BORaHW_x.d.ts} +5 -5
- package/dist/verifier-B-WFDMz6.d.cts +48 -0
- package/dist/verifier-B-WFDMz6.d.ts +48 -0
- 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.map +1 -1
- package/package.json +30 -30
- package/dist/gateway/client/index.cjs +0 -492
- package/dist/gateway/client/index.cjs.map +0 -1
- package/dist/gateway/client/index.d.cts +0 -63
- package/dist/gateway/client/index.d.ts +0 -63
- package/dist/gateway/client/index.js +0 -489
- package/dist/gateway/client/index.js.map +0 -1
- package/dist/gateway/index.cjs +0 -16
- package/dist/gateway/index.cjs.map +0 -1
- package/dist/gateway/index.d.cts +0 -52
- package/dist/gateway/index.d.ts +0 -52
- package/dist/gateway/index.js +0 -10
- package/dist/gateway/index.js.map +0 -1
- package/dist/gateway/server/index.cjs +0 -1248
- package/dist/gateway/server/index.cjs.map +0 -1
- package/dist/gateway/server/index.d.cts +0 -31
- package/dist/gateway/server/index.d.ts +0 -31
- package/dist/gateway/server/index.js +0 -1233
- package/dist/gateway/server/index.js.map +0 -1
- package/gateway/proto/lp_rpc.proto +0 -542
|
@@ -0,0 +1,747 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var crypto = require('crypto');
|
|
4
|
+
var oauth4webapi = require('oauth4webapi');
|
|
5
|
+
var http = require('http');
|
|
6
|
+
|
|
7
|
+
// src/signer/webhook/types.ts
|
|
8
|
+
function isValidUsageIdentity(identity) {
|
|
9
|
+
return Boolean(
|
|
10
|
+
identity.issuer.trim() && identity.client_id.trim() && identity.usage_subject.trim() && identity.usage_subject_type.trim()
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
// src/errors.ts
|
|
15
|
+
var PmtHouseError = class extends Error {
|
|
16
|
+
status;
|
|
17
|
+
code;
|
|
18
|
+
details;
|
|
19
|
+
constructor(message, {
|
|
20
|
+
status = 500,
|
|
21
|
+
code = "pymthouse_error",
|
|
22
|
+
details
|
|
23
|
+
} = {}) {
|
|
24
|
+
super(message);
|
|
25
|
+
this.name = "PmtHouseError";
|
|
26
|
+
this.status = status;
|
|
27
|
+
this.code = code;
|
|
28
|
+
this.details = details;
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
// src/signer/webhook/identity.ts
|
|
33
|
+
var DEFAULT_WEBHOOK_IDENTITY_CLAIMS = {
|
|
34
|
+
claimClientId: "client_id",
|
|
35
|
+
claimUsageSubject: "sub",
|
|
36
|
+
usageSubjectType: "external_user_id"
|
|
37
|
+
};
|
|
38
|
+
function readClaim(payload, key) {
|
|
39
|
+
const value = payload[key];
|
|
40
|
+
if (typeof value === "string" && value.trim()) {
|
|
41
|
+
return value.trim();
|
|
42
|
+
}
|
|
43
|
+
if (typeof value === "number" || typeof value === "boolean" || typeof value === "bigint") {
|
|
44
|
+
return value.toString().trim();
|
|
45
|
+
}
|
|
46
|
+
return "";
|
|
47
|
+
}
|
|
48
|
+
function identityFromWebhookClaims(claims, mapping = {}) {
|
|
49
|
+
const claimClientId = mapping.claimClientId ?? DEFAULT_WEBHOOK_IDENTITY_CLAIMS.claimClientId;
|
|
50
|
+
const claimUsageSubject = mapping.claimUsageSubject ?? DEFAULT_WEBHOOK_IDENTITY_CLAIMS.claimUsageSubject;
|
|
51
|
+
const defaultUsageSubjectType = mapping.usageSubjectType ?? DEFAULT_WEBHOOK_IDENTITY_CLAIMS.usageSubjectType;
|
|
52
|
+
let clientId = readClaim(claims, claimClientId);
|
|
53
|
+
if (!clientId) {
|
|
54
|
+
clientId = readClaim(claims, "azp");
|
|
55
|
+
}
|
|
56
|
+
const usageSubject = readClaim(claims, claimUsageSubject);
|
|
57
|
+
let usageSubjectType = defaultUsageSubjectType;
|
|
58
|
+
const claimUsageSubjectType = readClaim(claims, "usage_subject_type");
|
|
59
|
+
if (claimUsageSubjectType) {
|
|
60
|
+
usageSubjectType = claimUsageSubjectType;
|
|
61
|
+
}
|
|
62
|
+
const identity = {
|
|
63
|
+
issuer: readClaim(claims, "iss"),
|
|
64
|
+
client_id: clientId,
|
|
65
|
+
usage_subject: usageSubject,
|
|
66
|
+
usage_subject_type: usageSubjectType
|
|
67
|
+
};
|
|
68
|
+
if (!identity.issuer || !identity.client_id || !identity.usage_subject) {
|
|
69
|
+
throw new PmtHouseError("JWT missing required identity claims", {
|
|
70
|
+
status: 403,
|
|
71
|
+
code: "invalid_identity"
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return identity;
|
|
75
|
+
}
|
|
76
|
+
function claimExpirySeconds(claims, fallbackTtlSeconds = 300) {
|
|
77
|
+
const exp = claims.exp;
|
|
78
|
+
if (typeof exp === "number" && Number.isFinite(exp)) {
|
|
79
|
+
return Math.trunc(exp);
|
|
80
|
+
}
|
|
81
|
+
if (typeof exp === "string" && exp.trim()) {
|
|
82
|
+
const parsed = Number(exp);
|
|
83
|
+
if (Number.isFinite(parsed)) {
|
|
84
|
+
return Math.trunc(parsed);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return Math.trunc(Date.now() / 1e3) + fallbackTtlSeconds;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// src/signer/webhook/payload.ts
|
|
91
|
+
function headerValueFromWebhookPayload(headers, name) {
|
|
92
|
+
if (!headers) {
|
|
93
|
+
return "";
|
|
94
|
+
}
|
|
95
|
+
const target = name.toLowerCase();
|
|
96
|
+
for (const [key, values] of Object.entries(headers)) {
|
|
97
|
+
if (key.toLowerCase() !== target) {
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (!Array.isArray(values)) {
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
for (const value of values) {
|
|
104
|
+
if (typeof value === "string" && value.trim()) {
|
|
105
|
+
return value.trim();
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return "";
|
|
110
|
+
}
|
|
111
|
+
function authorizationFromWebhookPayload(payload) {
|
|
112
|
+
const fromHeaders = headerValueFromWebhookPayload(
|
|
113
|
+
payload.headers,
|
|
114
|
+
"Authorization"
|
|
115
|
+
);
|
|
116
|
+
if (fromHeaders) {
|
|
117
|
+
return fromHeaders;
|
|
118
|
+
}
|
|
119
|
+
return payload.authorization?.trim() ?? "";
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// src/signer/webhook/authorize.ts
|
|
123
|
+
function authIdFromIdentity(identity) {
|
|
124
|
+
return `${identity.client_id}:${identity.usage_subject}`;
|
|
125
|
+
}
|
|
126
|
+
function timingSafeEqualStrings(a, b) {
|
|
127
|
+
const aBuffer = Buffer.from(a);
|
|
128
|
+
const bBuffer = Buffer.from(b);
|
|
129
|
+
if (aBuffer.length !== bBuffer.length) {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
return crypto.timingSafeEqual(aBuffer, bBuffer);
|
|
133
|
+
}
|
|
134
|
+
function authenticateWebhookCaller(request, secret) {
|
|
135
|
+
if (!secret.trim()) {
|
|
136
|
+
return false;
|
|
137
|
+
}
|
|
138
|
+
const trimmed = secret.trim();
|
|
139
|
+
const auth = request.headers.get("authorization")?.trim() ?? "";
|
|
140
|
+
if (auth === `Bearer ${trimmed}`) {
|
|
141
|
+
return true;
|
|
142
|
+
}
|
|
143
|
+
const apiKey = request.headers.get("x-api-key")?.trim() ?? "";
|
|
144
|
+
if (apiKey && timingSafeEqualStrings(apiKey, trimmed)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
const legacySecret = request.headers.get("x-webhook-secret")?.trim() ?? "";
|
|
148
|
+
if (legacySecret && timingSafeEqualStrings(legacySecret, trimmed)) {
|
|
149
|
+
return true;
|
|
150
|
+
}
|
|
151
|
+
return false;
|
|
152
|
+
}
|
|
153
|
+
function paymentWebhookJson(httpStatus, body) {
|
|
154
|
+
return new Response(JSON.stringify(body), {
|
|
155
|
+
status: httpStatus,
|
|
156
|
+
headers: { "Content-Type": "application/json" }
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
function rejectStatusFromError(err) {
|
|
160
|
+
if (err instanceof PmtHouseError) {
|
|
161
|
+
return {
|
|
162
|
+
status: err.status >= 400 && err.status < 600 ? err.status : 403,
|
|
163
|
+
reason: err.message
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
const reason = err instanceof Error ? err.message : "authorization rejected";
|
|
167
|
+
return { status: 403, reason };
|
|
168
|
+
}
|
|
169
|
+
async function handleRemoteSignerAuthorize(request, config) {
|
|
170
|
+
if (!authenticateWebhookCaller(request, config.webhookSecret)) {
|
|
171
|
+
return paymentWebhookJson(401, {
|
|
172
|
+
status: 401,
|
|
173
|
+
reason: "unauthorized webhook caller"
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
let payload;
|
|
177
|
+
try {
|
|
178
|
+
payload = await request.json();
|
|
179
|
+
} catch {
|
|
180
|
+
return paymentWebhookJson(400, {
|
|
181
|
+
status: 400,
|
|
182
|
+
reason: "invalid request json"
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
const authorization = authorizationFromWebhookPayload(payload) ?? "";
|
|
186
|
+
try {
|
|
187
|
+
const verified = await config.endUserAuth.verify({
|
|
188
|
+
authorization,
|
|
189
|
+
payload,
|
|
190
|
+
request
|
|
191
|
+
});
|
|
192
|
+
if (config.afterVerify) {
|
|
193
|
+
await config.afterVerify({
|
|
194
|
+
authorization,
|
|
195
|
+
payload,
|
|
196
|
+
request,
|
|
197
|
+
verified,
|
|
198
|
+
identity: verified.identity
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
return paymentWebhookJson(200, {
|
|
202
|
+
status: 200,
|
|
203
|
+
expiry: verified.expiry,
|
|
204
|
+
auth_id: authIdFromIdentity(verified.identity),
|
|
205
|
+
identity: verified.identity
|
|
206
|
+
});
|
|
207
|
+
} catch (err) {
|
|
208
|
+
const { status, reason } = rejectStatusFromError(err);
|
|
209
|
+
return paymentWebhookJson(200, {
|
|
210
|
+
status,
|
|
211
|
+
reason
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
function createRemoteSignerAuthorizeHandler(config) {
|
|
216
|
+
return (request) => handleRemoteSignerAuthorize(request, config);
|
|
217
|
+
}
|
|
218
|
+
async function routeRemoteSignerWebhookRequest(request, config) {
|
|
219
|
+
const url = new URL(request.url);
|
|
220
|
+
if (request.method === "POST" && url.pathname === "/authorize") {
|
|
221
|
+
return handleRemoteSignerAuthorize(request, config);
|
|
222
|
+
}
|
|
223
|
+
const adminRoutes = config.endUserAuth.adminRoutes ?? [];
|
|
224
|
+
for (const route of adminRoutes) {
|
|
225
|
+
if (request.method === route.method && url.pathname === route.pathname) {
|
|
226
|
+
return route.handler(request);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return null;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// src/signer/webhook/bearer.ts
|
|
233
|
+
function bearerTokenFromAuthorization(authorization) {
|
|
234
|
+
const trimmed = authorization.trim();
|
|
235
|
+
if (!trimmed) {
|
|
236
|
+
throw new Error("missing authorization");
|
|
237
|
+
}
|
|
238
|
+
const prefix = "Bearer ";
|
|
239
|
+
if (!trimmed.startsWith(prefix)) {
|
|
240
|
+
throw new Error("authorization must be Bearer token");
|
|
241
|
+
}
|
|
242
|
+
const token = trimmed.slice(prefix.length).trim();
|
|
243
|
+
if (!token) {
|
|
244
|
+
throw new Error("empty bearer token");
|
|
245
|
+
}
|
|
246
|
+
return token;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
// src/signer/webhook/adapters/api-key/verifier.ts
|
|
250
|
+
function createApiKeyEndUserVerifier(config) {
|
|
251
|
+
const prefix = config.apiKeyPrefix ?? "sk_";
|
|
252
|
+
const defaultClientId = config.defaultClientId ?? "daydream-scope";
|
|
253
|
+
const defaultUsageSubjectType = config.defaultUsageSubjectType ?? "clerk_user_id";
|
|
254
|
+
const ttl = config.expiryTtlSeconds ?? 60;
|
|
255
|
+
return {
|
|
256
|
+
kind: "custom",
|
|
257
|
+
verify: async ({ authorization }) => {
|
|
258
|
+
const token = bearerTokenFromAuthorization(authorization);
|
|
259
|
+
if (prefix && !token.startsWith(prefix)) {
|
|
260
|
+
throw new PmtHouseError("invalid api key", {
|
|
261
|
+
status: 401,
|
|
262
|
+
code: "invalid_api_key"
|
|
263
|
+
});
|
|
264
|
+
}
|
|
265
|
+
const resolved = await config.resolveApiKey(token);
|
|
266
|
+
if (!resolved?.userId) {
|
|
267
|
+
throw new PmtHouseError("invalid api key", {
|
|
268
|
+
status: 401,
|
|
269
|
+
code: "invalid_api_key"
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
const identity = {
|
|
273
|
+
issuer: config.issuer,
|
|
274
|
+
client_id: resolved.clientId ?? defaultClientId,
|
|
275
|
+
usage_subject: resolved.userId,
|
|
276
|
+
usage_subject_type: resolved.usageSubjectType ?? defaultUsageSubjectType
|
|
277
|
+
};
|
|
278
|
+
return {
|
|
279
|
+
identity,
|
|
280
|
+
expiry: Math.trunc(Date.now() / 1e3) + ttl,
|
|
281
|
+
raw: resolved
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
};
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// src/signer/webhook/adapters/composite/verifier.ts
|
|
288
|
+
function createFirstMatchEndUserVerifier(verifiers) {
|
|
289
|
+
if (verifiers.length === 0) {
|
|
290
|
+
throw new PmtHouseError("at least one verifier is required", {
|
|
291
|
+
status: 500,
|
|
292
|
+
code: "invalid_verifier_config"
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
return {
|
|
296
|
+
kind: "custom",
|
|
297
|
+
verify: async (context) => {
|
|
298
|
+
let lastError;
|
|
299
|
+
for (const verifier of verifiers) {
|
|
300
|
+
try {
|
|
301
|
+
return await verifier.verify(context);
|
|
302
|
+
} catch (err) {
|
|
303
|
+
lastError = err;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
if (lastError instanceof PmtHouseError) {
|
|
307
|
+
throw lastError;
|
|
308
|
+
}
|
|
309
|
+
if (lastError instanceof Error) {
|
|
310
|
+
throw new PmtHouseError(lastError.message, {
|
|
311
|
+
status: 401,
|
|
312
|
+
code: "invalid_credentials"
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
throw new PmtHouseError("invalid credentials", {
|
|
316
|
+
status: 401,
|
|
317
|
+
code: "invalid_credentials"
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
adminRoutes: verifiers.flatMap((verifier) => verifier.adminRoutes ?? [])
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
// src/string-utils.ts
|
|
325
|
+
function stripTrailingSlashes(value) {
|
|
326
|
+
let end = value.length;
|
|
327
|
+
while (end > 0 && (value.codePointAt(end - 1) ?? 0) === 47) {
|
|
328
|
+
end--;
|
|
329
|
+
}
|
|
330
|
+
return value.slice(0, end);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// src/discovery.ts
|
|
334
|
+
var CACHE_TTL_MS = 5 * 60 * 1e3;
|
|
335
|
+
var discoveryCache = /* @__PURE__ */ new Map();
|
|
336
|
+
function normalizedIssuerKey(issuerUrl) {
|
|
337
|
+
return stripTrailingSlashes(issuerUrl);
|
|
338
|
+
}
|
|
339
|
+
async function loadAuthorizationServer(issuerUrl, fetchImpl, options = {}) {
|
|
340
|
+
const key = normalizedIssuerKey(issuerUrl);
|
|
341
|
+
const now = Date.now();
|
|
342
|
+
const cached = discoveryCache.get(key);
|
|
343
|
+
if (!options.force && cached && now - cached.fetchedAt < CACHE_TTL_MS) {
|
|
344
|
+
return cached.as;
|
|
345
|
+
}
|
|
346
|
+
const issuerIdentifier = new URL(key);
|
|
347
|
+
const discoveryOpts = {
|
|
348
|
+
algorithm: "oidc",
|
|
349
|
+
[oauth4webapi.customFetch]: fetchImpl
|
|
350
|
+
};
|
|
351
|
+
if (options.allowInsecureHttp) {
|
|
352
|
+
discoveryOpts[oauth4webapi.allowInsecureRequests] = true;
|
|
353
|
+
}
|
|
354
|
+
let response;
|
|
355
|
+
try {
|
|
356
|
+
response = await oauth4webapi.discoveryRequest(issuerIdentifier, discoveryOpts);
|
|
357
|
+
} catch (e) {
|
|
358
|
+
throw mapDiscoveryNetworkError(e);
|
|
359
|
+
}
|
|
360
|
+
let as;
|
|
361
|
+
try {
|
|
362
|
+
as = await oauth4webapi.processDiscoveryResponse(issuerIdentifier, response);
|
|
363
|
+
} catch (e) {
|
|
364
|
+
throw mapOAuthDiscoveryError(e);
|
|
365
|
+
}
|
|
366
|
+
discoveryCache.set(key, { as, fetchedAt: now });
|
|
367
|
+
return as;
|
|
368
|
+
}
|
|
369
|
+
function mapOAuthDiscoveryError(error) {
|
|
370
|
+
if (error instanceof PmtHouseError) {
|
|
371
|
+
return error;
|
|
372
|
+
}
|
|
373
|
+
if (error instanceof Error) {
|
|
374
|
+
return new PmtHouseError(error.message, {
|
|
375
|
+
status: 500,
|
|
376
|
+
code: "oidc_discovery_invalid",
|
|
377
|
+
details: { cause: error.cause }
|
|
378
|
+
});
|
|
379
|
+
}
|
|
380
|
+
return new PmtHouseError("OIDC discovery failed", {
|
|
381
|
+
status: 500,
|
|
382
|
+
code: "oidc_discovery_invalid"
|
|
383
|
+
});
|
|
384
|
+
}
|
|
385
|
+
function mapDiscoveryNetworkError(error) {
|
|
386
|
+
if (error instanceof PmtHouseError) {
|
|
387
|
+
return error;
|
|
388
|
+
}
|
|
389
|
+
if (error instanceof Error) {
|
|
390
|
+
return new PmtHouseError(`Failed to load OIDC discovery: ${error.message}`, {
|
|
391
|
+
status: 502,
|
|
392
|
+
code: "oidc_discovery_failed"
|
|
393
|
+
});
|
|
394
|
+
}
|
|
395
|
+
return new PmtHouseError("Failed to load OIDC discovery", {
|
|
396
|
+
status: 502,
|
|
397
|
+
code: "oidc_discovery_failed"
|
|
398
|
+
});
|
|
399
|
+
}
|
|
400
|
+
function mapOAuthError(error) {
|
|
401
|
+
if (error instanceof PmtHouseError) {
|
|
402
|
+
return error;
|
|
403
|
+
}
|
|
404
|
+
if (error instanceof oauth4webapi.ResponseBodyError) {
|
|
405
|
+
const cause = error.cause;
|
|
406
|
+
const description = typeof error.error_description === "string" ? error.error_description : error.message;
|
|
407
|
+
const details = { ...cause };
|
|
408
|
+
if (typeof cause.error_uri === "string") {
|
|
409
|
+
details.error_uri = cause.error_uri;
|
|
410
|
+
}
|
|
411
|
+
return new PmtHouseError(description, {
|
|
412
|
+
status: error.status,
|
|
413
|
+
code: error.error,
|
|
414
|
+
details
|
|
415
|
+
});
|
|
416
|
+
}
|
|
417
|
+
if (error instanceof oauth4webapi.OperationProcessingError) {
|
|
418
|
+
return new PmtHouseError(error.message, {
|
|
419
|
+
status: 502,
|
|
420
|
+
code: error.code ?? "oauth_processing_error",
|
|
421
|
+
details: { cause: error.cause }
|
|
422
|
+
});
|
|
423
|
+
}
|
|
424
|
+
if (error instanceof Error) {
|
|
425
|
+
return new PmtHouseError(error.message, {
|
|
426
|
+
status: 500,
|
|
427
|
+
code: "unexpected_error"
|
|
428
|
+
});
|
|
429
|
+
}
|
|
430
|
+
return new PmtHouseError("Unexpected error", {
|
|
431
|
+
status: 500,
|
|
432
|
+
code: "unexpected_error"
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
// src/verify.ts
|
|
437
|
+
async function verifyJwt(token, options) {
|
|
438
|
+
const fetchImpl = options.fetch ?? fetch;
|
|
439
|
+
const as = await loadAuthorizationServer(options.issuerUrl, fetchImpl, {
|
|
440
|
+
allowInsecureHttp: options.allowInsecureHttp
|
|
441
|
+
});
|
|
442
|
+
const request = new Request("https://resource.invalid/", {
|
|
443
|
+
headers: {
|
|
444
|
+
Authorization: `Bearer ${token}`
|
|
445
|
+
}
|
|
446
|
+
});
|
|
447
|
+
const httpOpts = {
|
|
448
|
+
[oauth4webapi.customFetch]: fetchImpl
|
|
449
|
+
};
|
|
450
|
+
if (options.allowInsecureHttp) {
|
|
451
|
+
httpOpts[oauth4webapi.allowInsecureRequests] = true;
|
|
452
|
+
}
|
|
453
|
+
try {
|
|
454
|
+
const claims = await oauth4webapi.validateJwtAccessToken(
|
|
455
|
+
as,
|
|
456
|
+
request,
|
|
457
|
+
options.audience,
|
|
458
|
+
httpOpts
|
|
459
|
+
);
|
|
460
|
+
if (options.requiredScopes?.length) {
|
|
461
|
+
const scopeStr = typeof claims.scope === "string" ? claims.scope : "";
|
|
462
|
+
const have = new Set(scopeStr.split(/\s+/).filter(Boolean));
|
|
463
|
+
for (const s of options.requiredScopes) {
|
|
464
|
+
if (!have.has(s)) {
|
|
465
|
+
throw new PmtHouseError(`Missing required scope: ${s}`, {
|
|
466
|
+
status: 403,
|
|
467
|
+
code: "insufficient_scope"
|
|
468
|
+
});
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
}
|
|
472
|
+
return claims;
|
|
473
|
+
} catch (e) {
|
|
474
|
+
throw mapOAuthError(e);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
// src/signer/webhook/adapters/oidc/verifier.ts
|
|
479
|
+
async function handleRemoteSignerRefreshJwks(request, config) {
|
|
480
|
+
if (!authenticateWebhookCaller(request, config.webhookSecret)) {
|
|
481
|
+
return new Response("unauthorized", { status: 401 });
|
|
482
|
+
}
|
|
483
|
+
try {
|
|
484
|
+
await loadAuthorizationServer(config.jwtIssuer, config.fetch ?? fetch, {
|
|
485
|
+
force: true,
|
|
486
|
+
allowInsecureHttp: config.allowInsecureHttp
|
|
487
|
+
});
|
|
488
|
+
return new Response(JSON.stringify({ status: "ok" }), {
|
|
489
|
+
status: 200,
|
|
490
|
+
headers: { "Content-Type": "application/json" }
|
|
491
|
+
});
|
|
492
|
+
} catch (err) {
|
|
493
|
+
const message = err instanceof Error ? err.message : "jwks refresh failed";
|
|
494
|
+
return new Response(message, { status: 500 });
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
function createOidcEndUserVerifier(config) {
|
|
498
|
+
const claimMapping = {
|
|
499
|
+
...DEFAULT_WEBHOOK_IDENTITY_CLAIMS,
|
|
500
|
+
...config.claimMapping
|
|
501
|
+
};
|
|
502
|
+
return {
|
|
503
|
+
kind: "oidc",
|
|
504
|
+
verify: async ({ authorization }) => {
|
|
505
|
+
const token = bearerTokenFromAuthorization(authorization);
|
|
506
|
+
const audience = config.jwtAudience.trim();
|
|
507
|
+
if (!audience) {
|
|
508
|
+
throw new Error("jwt audience is required for webhook verification");
|
|
509
|
+
}
|
|
510
|
+
const claims = await verifyJwt(token, {
|
|
511
|
+
issuerUrl: config.jwtIssuer,
|
|
512
|
+
audience,
|
|
513
|
+
fetch: config.fetch,
|
|
514
|
+
allowInsecureHttp: config.allowInsecureHttp,
|
|
515
|
+
requiredScopes: config.requiredScopes
|
|
516
|
+
});
|
|
517
|
+
const claimsRecord = claims;
|
|
518
|
+
const identity = identityFromWebhookClaims(claimsRecord, claimMapping);
|
|
519
|
+
return {
|
|
520
|
+
identity,
|
|
521
|
+
expiry: claimExpirySeconds(claimsRecord),
|
|
522
|
+
raw: claimsRecord
|
|
523
|
+
};
|
|
524
|
+
},
|
|
525
|
+
adminRoutes: [
|
|
526
|
+
{
|
|
527
|
+
method: "POST",
|
|
528
|
+
pathname: "/admin/refresh-jwks",
|
|
529
|
+
handler: (request) => handleRemoteSignerRefreshJwks(request, config)
|
|
530
|
+
}
|
|
531
|
+
]
|
|
532
|
+
};
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
// src/signer/webhook/adapters/oauth1/verifier.ts
|
|
536
|
+
function createOAuth1EndUserVerifier(config) {
|
|
537
|
+
return {
|
|
538
|
+
kind: "oauth1",
|
|
539
|
+
verify: async () => {
|
|
540
|
+
if (!config.consumerKey.trim() || !config.consumerSecret.trim()) {
|
|
541
|
+
throw new Error("OAuth 1.0 consumer credentials are required");
|
|
542
|
+
}
|
|
543
|
+
throw new Error("OAuth 1.0 webhook verification is not implemented yet");
|
|
544
|
+
}
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// src/signer/webhook/adapters/trusted-headers/verifier.ts
|
|
549
|
+
var DEFAULT_DMZ_TRUSTED_HEADERS = {
|
|
550
|
+
issuer: "X-Livepeer-Usage-Issuer",
|
|
551
|
+
clientId: "X-Livepeer-Client-ID",
|
|
552
|
+
usageSubject: "X-Livepeer-Usage-Subject",
|
|
553
|
+
usageSubjectType: "X-Livepeer-Usage-Subject-Type"
|
|
554
|
+
};
|
|
555
|
+
function normalizeIssuer(value) {
|
|
556
|
+
let end = value.length;
|
|
557
|
+
while (end > 0 && value[end - 1] === "/") {
|
|
558
|
+
end -= 1;
|
|
559
|
+
}
|
|
560
|
+
return value.slice(0, end);
|
|
561
|
+
}
|
|
562
|
+
function identityFromTrustedHeaders(headers, config) {
|
|
563
|
+
const names = {
|
|
564
|
+
...DEFAULT_DMZ_TRUSTED_HEADERS,
|
|
565
|
+
...config.headerNames
|
|
566
|
+
};
|
|
567
|
+
const issuer = headerValueFromWebhookPayload(headers, names.issuer);
|
|
568
|
+
const clientId = headerValueFromWebhookPayload(headers, names.clientId);
|
|
569
|
+
const usageSubject = headerValueFromWebhookPayload(headers, names.usageSubject);
|
|
570
|
+
const usageSubjectType = headerValueFromWebhookPayload(headers, names.usageSubjectType) || "external_user_id";
|
|
571
|
+
if (!issuer || !clientId || !usageSubject) {
|
|
572
|
+
throw new PmtHouseError("missing trusted usage identity headers", {
|
|
573
|
+
status: 403,
|
|
574
|
+
code: "invalid_identity"
|
|
575
|
+
});
|
|
576
|
+
}
|
|
577
|
+
if (normalizeIssuer(issuer) !== normalizeIssuer(config.expectedIssuer.trim())) {
|
|
578
|
+
throw new PmtHouseError("trusted usage issuer mismatch", {
|
|
579
|
+
status: 403,
|
|
580
|
+
code: "invalid_identity"
|
|
581
|
+
});
|
|
582
|
+
}
|
|
583
|
+
return {
|
|
584
|
+
issuer,
|
|
585
|
+
client_id: clientId,
|
|
586
|
+
usage_subject: usageSubject,
|
|
587
|
+
usage_subject_type: usageSubjectType
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
function createTrustedHeadersEndUserVerifier(config) {
|
|
591
|
+
const expiryTtlSeconds = config.expiryTtlSeconds ?? 300;
|
|
592
|
+
return {
|
|
593
|
+
kind: "trusted_headers",
|
|
594
|
+
verify: async ({ payload }) => {
|
|
595
|
+
const identity = identityFromTrustedHeaders(payload.headers, config);
|
|
596
|
+
return {
|
|
597
|
+
identity,
|
|
598
|
+
expiry: Math.trunc(Date.now() / 1e3) + expiryTtlSeconds
|
|
599
|
+
};
|
|
600
|
+
}
|
|
601
|
+
};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
// src/signer/webhook/adapters/oidc/config.ts
|
|
605
|
+
function envTrim(env, key) {
|
|
606
|
+
const value = env[key]?.trim();
|
|
607
|
+
return value || void 0;
|
|
608
|
+
}
|
|
609
|
+
function createOidcRemoteSignerWebhookConfig(input) {
|
|
610
|
+
const { afterVerify, ...oidcConfig } = input;
|
|
611
|
+
return {
|
|
612
|
+
webhookSecret: oidcConfig.webhookSecret,
|
|
613
|
+
endUserAuth: createOidcEndUserVerifier(oidcConfig),
|
|
614
|
+
afterVerify
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function createSignerDmzRemoteSignerWebhookConfig(input) {
|
|
618
|
+
const {
|
|
619
|
+
afterVerify,
|
|
620
|
+
dmzTrustedHeaders = true,
|
|
621
|
+
trustedHeaders,
|
|
622
|
+
...oidcConfig
|
|
623
|
+
} = input;
|
|
624
|
+
const oidcVerifier = createOidcEndUserVerifier(oidcConfig);
|
|
625
|
+
const endUserAuth = dmzTrustedHeaders === false ? oidcVerifier : createFirstMatchEndUserVerifier([
|
|
626
|
+
createTrustedHeadersEndUserVerifier({
|
|
627
|
+
expectedIssuer: oidcConfig.jwtIssuer,
|
|
628
|
+
...trustedHeaders
|
|
629
|
+
}),
|
|
630
|
+
oidcVerifier
|
|
631
|
+
]);
|
|
632
|
+
return {
|
|
633
|
+
webhookSecret: oidcConfig.webhookSecret,
|
|
634
|
+
endUserAuth,
|
|
635
|
+
afterVerify
|
|
636
|
+
};
|
|
637
|
+
}
|
|
638
|
+
function readOidcRemoteSignerWebhookConfigFromEnv(env = process.env) {
|
|
639
|
+
const webhookSecret = envTrim(env, "WEBHOOK_SECRET");
|
|
640
|
+
const jwtIssuer = envTrim(env, "JWT_ISSUER");
|
|
641
|
+
const jwtAudience = envTrim(env, "JWT_AUDIENCE") ?? jwtIssuer;
|
|
642
|
+
if (!webhookSecret) {
|
|
643
|
+
throw new Error("WEBHOOK_SECRET is required");
|
|
644
|
+
}
|
|
645
|
+
if (!jwtIssuer) {
|
|
646
|
+
throw new Error("JWT_ISSUER is required");
|
|
647
|
+
}
|
|
648
|
+
if (!jwtAudience) {
|
|
649
|
+
throw new Error("JWT_AUDIENCE is required");
|
|
650
|
+
}
|
|
651
|
+
return createSignerDmzRemoteSignerWebhookConfig({
|
|
652
|
+
webhookSecret,
|
|
653
|
+
jwtIssuer,
|
|
654
|
+
jwtAudience,
|
|
655
|
+
claimMapping: {
|
|
656
|
+
claimClientId: envTrim(env, "CLAIM_CLIENT_ID") ?? "client_id",
|
|
657
|
+
claimUsageSubject: envTrim(env, "CLAIM_USAGE_SUBJECT") ?? "sub",
|
|
658
|
+
usageSubjectType: envTrim(env, "USAGE_SUBJECT_TYPE") ?? "external_user_id"
|
|
659
|
+
},
|
|
660
|
+
allowInsecureHttp: envTrim(env, "ALLOW_INSECURE_HTTP") === "1"
|
|
661
|
+
});
|
|
662
|
+
}
|
|
663
|
+
var readRemoteSignerWebhookConfigFromEnv = readOidcRemoteSignerWebhookConfigFromEnv;
|
|
664
|
+
function startRemoteSignerWebhookServer(options = {}) {
|
|
665
|
+
const config = options.config ?? readOidcRemoteSignerWebhookConfigFromEnv();
|
|
666
|
+
const port = options.port ?? Number(process.env.PORT ?? 8090);
|
|
667
|
+
const addr = options.addr ?? process.env.ADDR ?? "0.0.0.0";
|
|
668
|
+
const server = http.createServer(async (req, res) => {
|
|
669
|
+
try {
|
|
670
|
+
const host = req.headers.host ?? "localhost";
|
|
671
|
+
const url = `http://${host}${req.url ?? "/"}`;
|
|
672
|
+
const headers = new Headers();
|
|
673
|
+
for (const [name, value] of Object.entries(req.headers)) {
|
|
674
|
+
if (typeof value === "string") {
|
|
675
|
+
headers.set(name, value);
|
|
676
|
+
} else if (Array.isArray(value)) {
|
|
677
|
+
for (const item of value) {
|
|
678
|
+
headers.append(name, item);
|
|
679
|
+
}
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
const chunks = [];
|
|
683
|
+
for await (const chunk of req) {
|
|
684
|
+
chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
685
|
+
}
|
|
686
|
+
const body = Buffer.concat(chunks);
|
|
687
|
+
const request = new Request(url, {
|
|
688
|
+
method: req.method,
|
|
689
|
+
headers,
|
|
690
|
+
body: req.method === "GET" || req.method === "HEAD" ? void 0 : body
|
|
691
|
+
});
|
|
692
|
+
const response = await routeRemoteSignerWebhookRequest(request, config) ?? new Response("not found", { status: 404 });
|
|
693
|
+
res.statusCode = response.status;
|
|
694
|
+
response.headers.forEach((value, name) => {
|
|
695
|
+
res.setHeader(name, value);
|
|
696
|
+
});
|
|
697
|
+
const responseBody = Buffer.from(await response.arrayBuffer());
|
|
698
|
+
res.end(responseBody);
|
|
699
|
+
} catch (err) {
|
|
700
|
+
const message = err instanceof Error ? err.message : "internal error";
|
|
701
|
+
res.statusCode = 500;
|
|
702
|
+
res.end(message);
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
server.listen(port, addr, () => {
|
|
706
|
+
console.log(
|
|
707
|
+
`[builder-sdk] remote signer identity webhook listening on http://${addr}:${port}`
|
|
708
|
+
);
|
|
709
|
+
console.log(
|
|
710
|
+
`[builder-sdk] end-user auth strategy=${config.endUserAuth.kind}`
|
|
711
|
+
);
|
|
712
|
+
});
|
|
713
|
+
return server;
|
|
714
|
+
}
|
|
715
|
+
|
|
716
|
+
// src/signer/webhook/index.ts
|
|
717
|
+
function createRemoteSignerRefreshJwksHandler(config) {
|
|
718
|
+
return (request) => handleRemoteSignerRefreshJwks(request, config);
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
exports.DEFAULT_DMZ_TRUSTED_HEADERS = DEFAULT_DMZ_TRUSTED_HEADERS;
|
|
722
|
+
exports.DEFAULT_WEBHOOK_IDENTITY_CLAIMS = DEFAULT_WEBHOOK_IDENTITY_CLAIMS;
|
|
723
|
+
exports.authenticateWebhookCaller = authenticateWebhookCaller;
|
|
724
|
+
exports.authorizationFromWebhookPayload = authorizationFromWebhookPayload;
|
|
725
|
+
exports.bearerTokenFromAuthorization = bearerTokenFromAuthorization;
|
|
726
|
+
exports.claimExpirySeconds = claimExpirySeconds;
|
|
727
|
+
exports.createApiKeyEndUserVerifier = createApiKeyEndUserVerifier;
|
|
728
|
+
exports.createFirstMatchEndUserVerifier = createFirstMatchEndUserVerifier;
|
|
729
|
+
exports.createOAuth1EndUserVerifier = createOAuth1EndUserVerifier;
|
|
730
|
+
exports.createOidcEndUserVerifier = createOidcEndUserVerifier;
|
|
731
|
+
exports.createOidcRemoteSignerWebhookConfig = createOidcRemoteSignerWebhookConfig;
|
|
732
|
+
exports.createRemoteSignerAuthorizeHandler = createRemoteSignerAuthorizeHandler;
|
|
733
|
+
exports.createRemoteSignerRefreshJwksHandler = createRemoteSignerRefreshJwksHandler;
|
|
734
|
+
exports.createSignerDmzRemoteSignerWebhookConfig = createSignerDmzRemoteSignerWebhookConfig;
|
|
735
|
+
exports.createTrustedHeadersEndUserVerifier = createTrustedHeadersEndUserVerifier;
|
|
736
|
+
exports.handleRemoteSignerAuthorize = handleRemoteSignerAuthorize;
|
|
737
|
+
exports.handleRemoteSignerRefreshJwks = handleRemoteSignerRefreshJwks;
|
|
738
|
+
exports.headerValueFromWebhookPayload = headerValueFromWebhookPayload;
|
|
739
|
+
exports.identityFromTrustedHeaders = identityFromTrustedHeaders;
|
|
740
|
+
exports.identityFromWebhookClaims = identityFromWebhookClaims;
|
|
741
|
+
exports.isValidUsageIdentity = isValidUsageIdentity;
|
|
742
|
+
exports.readOidcRemoteSignerWebhookConfigFromEnv = readOidcRemoteSignerWebhookConfigFromEnv;
|
|
743
|
+
exports.readRemoteSignerWebhookConfigFromEnv = readRemoteSignerWebhookConfigFromEnv;
|
|
744
|
+
exports.routeRemoteSignerWebhookRequest = routeRemoteSignerWebhookRequest;
|
|
745
|
+
exports.startRemoteSignerWebhookServer = startRemoteSignerWebhookServer;
|
|
746
|
+
//# sourceMappingURL=webhook.cjs.map
|
|
747
|
+
//# sourceMappingURL=webhook.cjs.map
|