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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "poe-code",
3
- "version": "4.0.23",
3
+ "version": "4.0.25",
4
4
  "description": "CLI tool to configure Poe API for developer workflows.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -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
- return `<label>${escapeHtml(field.label ?? field.name)}<input name="${name}" type="${field.type ?? "text"}" required autocomplete="${field.type === "password" ? "current-password" : "off"}"></label>`;
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
- return `<!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}${error === undefined ? "" : ".error{color:#b42318}"}</style></head><body><h1>Connect ${escapeHtml(providerName)}</h1>${error === undefined ? "" : `<p class="error">${escapeHtml(error)}</p>`}<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 type="submit">Connect</button></form></body></html>`;
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
- return new Response(renderLogin(displayName, fields, transaction, security.csrfToken), {
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": loginContentSecurityPolicy(transaction),
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
- response.writeHead(200, { "content-type": "application/json", "cache-control": "no-store" });
221
- response.end('{"ok":true}');
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/plain; charset=utf-8",
264
- "cache-control": "no-store"
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("This connection has expired or was already used. Restart the connection.");
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": loginContentSecurityPolicy(transaction),
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(renderLogin(displayName, fields, transaction, csrf, safeError));
339
+ response.end(login.html);
309
340
  }
310
341
  return true;
311
342
  }