job-application-agent 3.2.0 → 3.2.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.
@@ -74,6 +74,11 @@ function isPublicHostname(hostname) {
74
74
  return value.includes('.');
75
75
  }
76
76
 
77
+ function isReservedExampleHostname(hostname) {
78
+ const value = hostname.toLowerCase();
79
+ return ['example.com', 'example.net', 'example.org'].some((suffix) => value === suffix || value.endsWith(`.${suffix}`));
80
+ }
81
+
77
82
  function hostnameMatches(hostname, suffix) {
78
83
  return hostname === suffix || hostname.endsWith(`.${suffix}`);
79
84
  }
@@ -139,6 +144,7 @@ export function normalizeCommunityJob(input) {
139
144
  if (url.protocol !== 'https:' || url.username || url.password) throw new Error('community job.url must be a public HTTPS URL.');
140
145
  url.hostname = url.hostname.replace(/\.+$/, '').toLowerCase();
141
146
  if (!isPublicHostname(url.hostname)) throw new Error('community job.url must use a public HTTPS hostname.');
147
+ if (isReservedExampleHostname(url.hostname)) throw new Error('community job.url must not use a reserved example hostname.');
142
148
  const pathname = decodedPathname(url.pathname).normalize('NFKC');
143
149
  url.pathname = pathname;
144
150
  if (containsEmailLike(pathname) || containsPhoneLikeLocation(url.hostname, pathname) || looksIdentityPath(pathname) || looksPersonal(url)) throw new Error('community job.url must not be a personal URL.');
@@ -1 +1 @@
1
- export const SKILL_VERSION = '3.2.0';
1
+ export const SKILL_VERSION = '3.2.1';
@@ -48,6 +48,14 @@ const matchingJob = {
48
48
  ],
49
49
  };
50
50
 
51
+ function isolatedCliEnv(directory) {
52
+ return {
53
+ ...process.env,
54
+ JOB_APPLICATION_AGENT_STATE_DIR: directory,
55
+ JOB_APPLICATION_AGENT_SOURCE_COMMUNITY_URL: 'http://127.0.0.1:9',
56
+ };
57
+ }
58
+
51
59
  function runCli(script, args, input, env) {
52
60
  return new Promise((resolve) => {
53
61
  const child = spawn(process.execPath, [script, ...args], { env, stdio: ['pipe', 'pipe', 'pipe'] });
@@ -73,7 +81,7 @@ test('returns the canonical resume path for direct browser uploads', async (t) =
73
81
  const resume = join(directory, 'resume.pdf');
74
82
  await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
75
83
  await writeFile(resume, '%PDF-1.7\ncanonical resume fixture');
76
- const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
84
+ const env = isolatedCliEnv(directory);
77
85
 
78
86
  const result = JSON.parse(execFileSync(process.execPath, [script, 'resume', 'path'], { env, encoding: 'utf8' }));
79
87
 
@@ -246,7 +254,7 @@ test('deduplicates ledger entries by normalized URL', async (t) => {
246
254
  telemetry: { durationBucket: '5-15m', fieldsFilled: 14, shortAnswerCount: 2, resumeUploaded: true },
247
255
  };
248
256
  await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
249
- const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
257
+ const env = isolatedCliEnv(directory);
250
258
  execFileSync(process.execPath, [script, 'ledger', 'add', '--stdin'], { input: JSON.stringify(entry), env, encoding: 'utf8' });
251
259
  const duplicate = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'check', '--stdin'], { input: JSON.stringify({ id: 'different', url: 'https://jobs.example.com/123?ref=friend' }), env, encoding: 'utf8' }));
252
260
  assert.equal(duplicate.duplicate, true);
@@ -263,7 +271,7 @@ test('warns on same-company role matches and serializes concurrent duplicate sub
263
271
  t.after(() => rm(directory, { recursive: true, force: true }));
264
272
  const script = fileURLToPath(new URL('../scripts/job-application.mjs', import.meta.url));
265
273
  await writeFile(join(directory, 'telemetry.json'), JSON.stringify({ version: 1, enabled: false, disclosed: true, graceConsumed: true, installationEventPending: false }));
266
- const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
274
+ const env = isolatedCliEnv(directory);
267
275
  const base = {
268
276
  company: 'Example', role: 'Senior Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 88,
269
277
  status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {}, employerJobId: 'example:123',
@@ -291,7 +299,7 @@ test('records structured outcomes idempotently without duplicate rows', async (t
291
299
  id: 'example-role-1', company: 'Example', role: 'Senior Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 88,
292
300
  status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
293
301
  })}\n`);
294
- const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
302
+ const env = isolatedCliEnv(directory);
295
303
  const outcome = { id: 'example-role-1', status: 'rejected', occurredAt: '2026-01-20T09:00:00Z', note: 'No sponsorship' };
296
304
  const enriched = { ...outcome, reasons: [{ category: 'eligibility', evidence: 'explicit' }] };
297
305
  const first = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], { input: JSON.stringify(outcome), env, encoding: 'utf8' }));
@@ -314,7 +322,7 @@ test('records bounded interview quality and failure-point enrichment idempotentl
314
322
  id: 'example-role-1', company: 'Example', role: 'Staff Product Engineer', url: 'https://jobs.example.com/123', source: 'company', score: 91,
315
323
  status: 'submitted', submittedAt: '2026-01-15T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
316
324
  })}\n`);
317
- const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
325
+ const env = isolatedCliEnv(directory);
318
326
  const base = { id: 'example-role-1', status: 'interview', occurredAt: '2026-01-20T09:00:00Z' };
319
327
  const enriched = { ...base, interviewQuality: 'weak', failurePoint: 'role-scope' };
320
328
  const first = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'outcome', '--stdin'], { input: JSON.stringify(base), env, encoding: 'utf8' }));
@@ -400,7 +408,7 @@ test('acknowledges a generated review only through the explicit CLI command', as
400
408
  source: 'company', score: 80, status: 'submitted', submittedAt: '2026-01-01T10:00:00Z', approval: 'STANDING AUTHORIZATION', answers: {},
401
409
  }));
402
410
  await writeFile(join(directory, 'applications.ndjson'), `${entries.map(JSON.stringify).join('\n')}\n`);
403
- const env = { ...process.env, JOB_APPLICATION_AGENT_STATE_DIR: directory };
411
+ const env = isolatedCliEnv(directory);
404
412
  const before = JSON.parse(execFileSync(process.execPath, [script, 'ledger', 'review'], { env, encoding: 'utf8' }));
405
413
  assert.equal(before.reviewDue, true);
406
414
  assert.equal(await readFile(join(directory, 'reviews.ndjson'), 'utf8').catch(() => ''), '');
@@ -53,6 +53,7 @@ test('accepts public job detail identifiers but rejects private, personal, and c
53
53
 
54
54
  const rejected = [
55
55
  'http://localhost/jobs/123',
56
+ 'https://jobs.example.com/123',
56
57
  'https://linkedin.com/in/some-person',
57
58
  'https://company.example/candidate/9876543210',
58
59
  'https://company.example/referral/12345678-1234-4123-8123-123456789abc',
@@ -61,7 +62,7 @@ test('accepts public job detail identifiers but rejects private, personal, and c
61
62
  'https://company.example/jobs/access-token=abcdefghijklmnop',
62
63
  'https://user:password@company.example/jobs/123',
63
64
  ];
64
- for (const url of rejected) assert.throws(() => normalizeCommunityJob({ ...job, url }), /public HTTPS|personal|credential/i, url);
65
+ for (const url of rejected) assert.throws(() => normalizeCommunityJob({ ...job, url }), /public HTTPS|personal|credential|reserved example/i, url);
65
66
  });
66
67
 
67
68
  test('preserves only stable query job identifiers while removing referral data', async () => {
@@ -56,7 +56,7 @@ function submission(index, roundId, overrides = {}) {
56
56
  id: `round-role-${index}`,
57
57
  company: `Company ${index}`,
58
58
  role: 'Senior Product Engineer',
59
- url: `https://jobs.example.com/${index}`,
59
+ url: `https://jobs.fixture.example/${index}`,
60
60
  employerJobId: `example:${index}`,
61
61
  source: 'ashby',
62
62
  discoverySource: 'linkedin',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "job-application-agent",
3
- "version": "3.2.0",
3
+ "version": "3.2.1",
4
4
  "description": "A privacy-first Agent Skill and CLI for evidence-based job discovery, application completion, and outcome tracking.",
5
5
  "license": "MIT",
6
6
  "type": "module",