poe-code 4.0.23 → 4.0.25
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/package.json
CHANGED
|
@@ -31,6 +31,7 @@ export interface HostedOAuthStorage<TCredential = unknown> {
|
|
|
31
31
|
capabilities: HostedOAuthStorageCapabilities;
|
|
32
32
|
signingKey(): Promise<OAuthAuthorizationServerSigningKey>;
|
|
33
33
|
resolveSubject(providerName: string, accountId: string): Promise<string>;
|
|
34
|
+
healthCheck?(): Promise<void>;
|
|
34
35
|
cleanup?(now?: number): Promise<void>;
|
|
35
36
|
}
|
|
36
37
|
export interface HostedOAuthCredentialAccess<TCredential = unknown> {
|
|
@@ -111,18 +111,30 @@ function loginField(field) {
|
|
|
111
111
|
type: field === "password" || field === "apiKey" ? "password" : field === "email" ? "email" : "text"
|
|
112
112
|
};
|
|
113
113
|
}
|
|
114
|
-
function renderLogin(providerName, fields, transaction, csrfToken, error) {
|
|
114
|
+
function renderLogin(providerName, fields, transaction, csrfToken, error, submittedValues = {}) {
|
|
115
|
+
const scriptNonce = randomBytes(18).toString("base64");
|
|
115
116
|
const controls = fields
|
|
116
117
|
.map((field) => {
|
|
117
118
|
const name = escapeHtml(field.name);
|
|
118
|
-
|
|
119
|
+
const type = field.type ?? "text";
|
|
120
|
+
const autocomplete = type === "email" ? "username" : field.name === "password" ? "current-password" : "off";
|
|
121
|
+
const value = type === "email" ? submittedValues[field.name] ?? "" : "";
|
|
122
|
+
return `<label>${escapeHtml(field.label ?? field.name)}<input name="${name}" type="${type}" required autocomplete="${autocomplete}"${value.length === 0 ? "" : ` value="${escapeHtml(value)}"`}></label>`;
|
|
119
123
|
})
|
|
120
124
|
.join("");
|
|
121
|
-
|
|
125
|
+
const idleLabel = `Connect ${providerName}`;
|
|
126
|
+
const html = `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Connect ${escapeHtml(providerName)}</title><style>body{font:16px system-ui;max-width:28rem;margin:10vh auto;padding:1rem;color:#171717}form{display:grid;gap:1rem}label{display:grid;gap:.35rem}input,button{font:inherit;padding:.7rem}button{cursor:pointer}button:disabled{cursor:wait;opacity:.7}.status{margin:0;color:#525252}.error{color:#b42318}</style></head><body><h1>Connect ${escapeHtml(providerName)}</h1>${error === undefined ? "" : `<p class="error" role="alert">${escapeHtml(error)}</p>`}<form id="oauth-connect-form" method="post" action="/oauth/connect"><input type="hidden" name="transaction" value="${escapeHtml(transaction.id)}"><input type="hidden" name="csrf" value="${escapeHtml(csrfToken)}">${controls}<button id="oauth-connect-button" type="submit" data-idle-label="${escapeHtml(idleLabel)}">${escapeHtml(idleLabel)}</button><p id="oauth-connect-status" class="status" role="status" aria-live="polite" hidden>Signing in… This may take a moment.</p></form><script nonce="${scriptNonce}">(()=>{const form=document.getElementById("oauth-connect-form");const button=document.getElementById("oauth-connect-button");const status=document.getElementById("oauth-connect-status");if(!(form instanceof HTMLFormElement)||!(button instanceof HTMLButtonElement)||!(status instanceof HTMLElement))return;const reset=()=>{form.removeAttribute("aria-busy");button.disabled=false;button.textContent=button.dataset.idleLabel||"Connect";status.hidden=true};form.addEventListener("submit",()=>{form.setAttribute("aria-busy","true");button.disabled=true;button.textContent="Connecting…";status.hidden=false});addEventListener("pageshow",reset)})();</script></body></html>`;
|
|
127
|
+
return {
|
|
128
|
+
contentSecurityPolicy: loginContentSecurityPolicy(transaction, scriptNonce),
|
|
129
|
+
html
|
|
130
|
+
};
|
|
122
131
|
}
|
|
123
|
-
function loginContentSecurityPolicy(transaction) {
|
|
132
|
+
function loginContentSecurityPolicy(transaction, scriptNonce) {
|
|
124
133
|
const callbackOrigin = new URL(transaction.redirectUri).origin;
|
|
125
|
-
return `default-src 'none'; style-src 'unsafe-inline'; form-action 'self' ${callbackOrigin}; base-uri 'none'; frame-ancestors 'none'`;
|
|
134
|
+
return `default-src 'none'; style-src 'unsafe-inline'; script-src 'nonce-${scriptNonce}'; form-action 'self' ${callbackOrigin}; base-uri 'none'; frame-ancestors 'none'`;
|
|
135
|
+
}
|
|
136
|
+
function renderExpiredConnection() {
|
|
137
|
+
return `<!doctype html><html><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1"><title>Connection expired</title><style>body{font:16px system-ui;max-width:28rem;margin:10vh auto;padding:1rem;color:#171717}p{line-height:1.5}</style></head><body><h1>Connection expired</h1><p>This sign-in link has expired or was already used.</p><p>Close this tab, return to the app that started the connection, and click Connect again.</p></body></html>`;
|
|
126
138
|
}
|
|
127
139
|
function interactionCookieName(transactionId) {
|
|
128
140
|
const suffix = createHash("sha256").update(transactionId).digest("base64url").slice(0, 22);
|
|
@@ -186,11 +198,12 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
186
198
|
const security = createAuthorizationInteractionSecurity({
|
|
187
199
|
cookieName: interactionCookieName(transaction.id)
|
|
188
200
|
});
|
|
189
|
-
|
|
201
|
+
const login = renderLogin(displayName, fields, transaction, security.csrfToken);
|
|
202
|
+
return new Response(login.html, {
|
|
190
203
|
status: 200,
|
|
191
204
|
headers: {
|
|
192
205
|
"content-type": "text/html; charset=utf-8",
|
|
193
|
-
"content-security-policy":
|
|
206
|
+
"content-security-policy": login.contentSecurityPolicy,
|
|
194
207
|
"cache-control": "no-store",
|
|
195
208
|
"referrer-policy": "no-referrer",
|
|
196
209
|
"x-content-type-options": "nosniff",
|
|
@@ -217,8 +230,21 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
217
230
|
const requestHandler = async (request, response) => {
|
|
218
231
|
const url = new URL(request.url ?? "/", prepared.issuer);
|
|
219
232
|
if (request.method === "GET" && url.pathname === "/healthz") {
|
|
220
|
-
|
|
221
|
-
|
|
233
|
+
try {
|
|
234
|
+
await config.storage.healthCheck?.();
|
|
235
|
+
response.writeHead(200, {
|
|
236
|
+
"content-type": "application/json",
|
|
237
|
+
"cache-control": "no-store"
|
|
238
|
+
});
|
|
239
|
+
response.end('{"ok":true}');
|
|
240
|
+
}
|
|
241
|
+
catch {
|
|
242
|
+
response.writeHead(503, {
|
|
243
|
+
"content-type": "application/json",
|
|
244
|
+
"cache-control": "no-store"
|
|
245
|
+
});
|
|
246
|
+
response.end('{"ok":false}');
|
|
247
|
+
}
|
|
222
248
|
return true;
|
|
223
249
|
}
|
|
224
250
|
if (customInteraction?.paths.includes(url.pathname) === true) {
|
|
@@ -227,11 +253,11 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
227
253
|
request: toWebRequest(request, prepared.issuer, body),
|
|
228
254
|
async complete({ transactionId, accountId, credential }) {
|
|
229
255
|
const subject = await config.storage.resolveSubject(config.provider.name, accountId);
|
|
256
|
+
await config.storage.credentials.set(subject, credential);
|
|
230
257
|
const completed = await authorizationServer.completeAuthorization({
|
|
231
258
|
transactionId,
|
|
232
259
|
subject
|
|
233
260
|
});
|
|
234
|
-
await config.storage.credentials.set(subject, credential);
|
|
235
261
|
await config.storage.interactions.delete(transactionId);
|
|
236
262
|
return new Response(null, {
|
|
237
263
|
status: 303,
|
|
@@ -260,10 +286,13 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
260
286
|
}) ||
|
|
261
287
|
transaction.expiresAt <= Date.now()) {
|
|
262
288
|
response.writeHead(400, {
|
|
263
|
-
"content-type": "text/
|
|
264
|
-
"
|
|
289
|
+
"content-type": "text/html; charset=utf-8",
|
|
290
|
+
"content-security-policy": "default-src 'none'; style-src 'unsafe-inline'; base-uri 'none'; frame-ancestors 'none'",
|
|
291
|
+
"cache-control": "no-store",
|
|
292
|
+
"referrer-policy": "no-referrer",
|
|
293
|
+
"x-content-type-options": "nosniff"
|
|
265
294
|
});
|
|
266
|
-
response.end(
|
|
295
|
+
response.end(renderExpiredConnection());
|
|
267
296
|
return true;
|
|
268
297
|
}
|
|
269
298
|
const controller = new AbortController();
|
|
@@ -281,11 +310,11 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
281
310
|
if (connected.accountId.trim().length === 0)
|
|
282
311
|
throw new Error("Provider returned an empty accountId.");
|
|
283
312
|
const subject = await config.storage.resolveSubject(config.provider.name, connected.accountId);
|
|
313
|
+
await config.storage.credentials.set(subject, connected.credential);
|
|
284
314
|
const completed = await authorizationServer.completeAuthorization({
|
|
285
315
|
transactionId,
|
|
286
316
|
subject
|
|
287
317
|
});
|
|
288
|
-
await config.storage.credentials.set(subject, connected.credential);
|
|
289
318
|
await config.storage.interactions.delete(transactionId);
|
|
290
319
|
response.writeHead(303, {
|
|
291
320
|
location: completed.redirectUrl.href,
|
|
@@ -299,13 +328,15 @@ export async function prepareHostedOAuthRuntime(config) {
|
|
|
299
328
|
const safeError = error instanceof HostedOAuthLoginError
|
|
300
329
|
? error.message
|
|
301
330
|
: "Sign-in failed. Please try again.";
|
|
331
|
+
const login = renderLogin(displayName, fields, transaction, csrf, safeError, values);
|
|
302
332
|
response.writeHead(400, {
|
|
303
333
|
"content-type": "text/html; charset=utf-8",
|
|
304
|
-
"content-security-policy":
|
|
334
|
+
"content-security-policy": login.contentSecurityPolicy,
|
|
305
335
|
"cache-control": "no-store",
|
|
306
|
-
"referrer-policy": "no-referrer"
|
|
336
|
+
"referrer-policy": "no-referrer",
|
|
337
|
+
"x-content-type-options": "nosniff"
|
|
307
338
|
});
|
|
308
|
-
response.end(
|
|
339
|
+
response.end(login.html);
|
|
309
340
|
}
|
|
310
341
|
return true;
|
|
311
342
|
}
|