@plosson/agentio 0.8.1 → 0.8.2

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": "@plosson/agentio",
3
- "version": "0.8.1",
3
+ "version": "0.8.2",
4
4
  "description": "CLI for LLM agents to interact with communication and tracking services",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -809,6 +809,29 @@ describe('handleAuthorizePost — happy path', () => {
809
809
  expect(url.searchParams.get('state')).toBe('STATE_XYZ');
810
810
  });
811
811
 
812
+ test('api_key with trailing whitespace still authorizes (trim)', async () => {
813
+ // Regression: operators often paste the key with a trailing newline
814
+ // from terminals. Byte-for-byte compare would silently reject.
815
+ const ctx = makeCtx();
816
+ const clientId = await registerTestClient(ctx);
817
+ for (const suffix of [' ', '\n', '\r\n', ' \t']) {
818
+ const res = await handleAuthorizePost(
819
+ authorizePost({
820
+ client_id: clientId,
821
+ redirect_uri: 'http://localhost:53682/callback',
822
+ response_type: 'code',
823
+ code_challenge: 'CHAL',
824
+ code_challenge_method: 'S256',
825
+ state: '',
826
+ scope: 'gchat:default',
827
+ api_key: TEST_API_KEY + suffix,
828
+ }),
829
+ ctx
830
+ );
831
+ expect(res.status).toBe(302);
832
+ }
833
+ });
834
+
812
835
  test('issued code is consumable from the store', async () => {
813
836
  const ctx = makeCtx();
814
837
  const clientId = await registerTestClient(ctx);
@@ -539,7 +539,10 @@ export async function handleAuthorizePost(
539
539
 
540
540
  const params = result.params;
541
541
  const client = ctx.oauthStore.findClient(params.clientId);
542
- const apiKey = form.get('api_key') ?? '';
542
+ // Trim pasted whitespace — same UX issue as setup-page.ts: a stray
543
+ // newline or trailing space from copy-paste would otherwise fail the
544
+ // byte-for-byte compare with a confusing error.
545
+ const apiKey = (form.get('api_key') ?? '').trim();
543
546
 
544
547
  if (!apiKey || !constantTimeEquals(apiKey, ctx.apiKey)) {
545
548
  // Re-render the form with an error. We deliberately do NOT redirect
@@ -160,6 +160,23 @@ describe('POST / — login', () => {
160
160
  expect(res.headers.get('set-cookie')).toBeNull();
161
161
  });
162
162
 
163
+ test('correct api_key with trailing whitespace (newline/space) → 302', async () => {
164
+ // Regression: operators routinely paste the key with a trailing
165
+ // newline from terminals. Byte-for-byte compare would silently
166
+ // reject an otherwise-valid key; trim makes it forgiving.
167
+ for (const suffix of [' ', '\n', '\r\n', ' \t']) {
168
+ const res = await handleRequest(
169
+ req('POST', '/', {
170
+ contentType: FORM,
171
+ body: formBody({ api_key: API_KEY + suffix }),
172
+ }),
173
+ ctx
174
+ );
175
+ expect(res.status).toBe(302);
176
+ expect(res.headers.get('set-cookie')).toContain(expectedCookieValue(API_KEY));
177
+ }
178
+ });
179
+
163
180
  test('correct api_key → 302 to /, sets cookie with expected attributes', async () => {
164
181
  const res = await handleRequest(
165
182
  req('POST', '/', {
@@ -338,7 +338,11 @@ async function handleSetupPost(
338
338
  });
339
339
  }
340
340
 
341
- const apiKey = form.get('api_key') ?? '';
341
+ // Trim: operators routinely paste the key with a trailing newline or
342
+ // space from terminals / password managers. constantTimeEquals is a
343
+ // byte-for-byte compare, so an extra whitespace character silently
344
+ // fails the login with a confusing "Invalid API key" error.
345
+ const apiKey = (form.get('api_key') ?? '').trim();
342
346
  if (!apiKey || !constantTimeEquals(apiKey, ctx.apiKey)) {
343
347
  return new Response(loginFormHtml('Invalid API key.'), {
344
348
  status: 401,