trackly-cli 0.10.1 → 0.10.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.
@@ -1,6 +1,5 @@
1
1
  'use strict';
2
2
 
3
- const { createHash } = require('node:crypto');
4
3
  const { z } = require('zod');
5
4
  const { apiRequest } = require('../lib/client');
6
5
  const { prepareResume, verifyPreparedResume } = require('../lib/agent');
@@ -20,25 +19,6 @@ const APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST = 20;
20
19
  const APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT = 25;
21
20
  const APPLY_BATCH_MAX_BULK_MUTATIONS = 20;
22
21
 
23
- function sensitiveRevocationConfirmation(profileResponse) {
24
- const profile = profileResponse?.profile || {};
25
- const currentRevision = Number(profile.revision);
26
- const fields = profile.fields || {};
27
- // Stored answer rows serialize an `encrypted` boolean plus their PERSISTED
28
- // row sensitivity — the same predicate the backend DELETE uses. Profile-column
29
- // and backfilled entries never carry `encrypted` and are never deleted.
30
- const affectedKeys = Object.keys(fields)
31
- .filter((key) => {
32
- const entry = fields[key];
33
- return entry && typeof entry.encrypted === 'boolean'
34
- && (entry.sensitivity === 'sensitive' || entry.sensitivity === 'restricted');
35
- })
36
- .sort();
37
- const confirmationToken = createHash('sha256')
38
- .update(`trackly:sensitive-revocation:v1:${currentRevision}:${affectedKeys.join(',')}`, 'utf8')
39
- .digest('hex');
40
- return { currentRevision, affectedKeys, confirmationToken };
41
- }
42
22
  const SAFE_OBSERVATION_CODE = /^[a-z0-9][a-z0-9_:-]{0,99}$/;
43
23
  const SAFE_IDEMPOTENCY_KEY = /^[\x20-\x7e]+$/;
44
24
 
@@ -174,7 +154,7 @@ function registerApplyTools(
174
154
 
175
155
  server.tool(
176
156
  'trackly_update_application_profile',
177
- 'Update confirmed profile answers with optimistic concurrency. Use global scope only for an explicit always-answer preference. Setting sensitiveStorageConsent=false permanently deletes every stored sensitive and restricted answer and is a two-step action: the first call saves nothing and returns a confirmation challenge; retry with the echoed sensitiveRevocationConfirmToken to proceed.',
157
+ 'Update confirmed profile answers with optimistic concurrency. Use global scope only for an explicit always-answer preference. Setting sensitiveStorageConsent=false deletes every stored sensitive and restricted answer (an admin-recoverable archive is kept for 30 days, then purged) and is a two-step action: the first call saves nothing and returns a confirmation challenge; retry with the echoed sensitiveRevocationConfirmToken to proceed.',
178
158
  {
179
159
  expectedRevision: z.number().int().min(1),
180
160
  source: z.enum(['web', 'ios', 'macos', 'codex', 'claude', 'mcp']).optional(),
@@ -207,52 +187,12 @@ function registerApplyTools(
207
187
  sensitiveRevocationConfirmToken: z.string().regex(/^[a-f0-9]{64}$/).optional(),
208
188
  },
209
189
  wrapTool(async (params) => {
210
- const { sensitiveRevocationConfirmToken, ...body } = params;
211
- if (body.sensitiveStorageConsent === false) {
212
- const profileResponse = await apiRequest('GET', '/api/jobscout/application-profile', null, false, false, MCP_USER_AGENT);
213
- const rawRevision = Number(profileResponse?.profile?.revision);
214
- if (!Number.isSafeInteger(rawRevision) || rawRevision < 1) {
215
- throw {
216
- status: 502,
217
- code: 'invalid_profile_revision',
218
- error: 'Trackly did not return a valid profile revision. No changes were saved.',
219
- };
220
- }
221
- const profileFieldsShape = profileResponse?.profile?.fields;
222
- const profileEntriesValid = !!profileFieldsShape
223
- && typeof profileFieldsShape === 'object'
224
- && !Array.isArray(profileFieldsShape)
225
- && Object.keys(profileFieldsShape).length > 0
226
- && Object.values(profileFieldsShape).every((entry) => entry
227
- && typeof entry === 'object' && !Array.isArray(entry)
228
- && typeof entry.state === 'string'
229
- && (entry.sensitivity === 'standard' || entry.sensitivity === 'sensitive' || entry.sensitivity === 'restricted')
230
- && (!('encrypted' in entry) || typeof entry.encrypted === 'boolean'));
231
- if (!profileEntriesValid) {
232
- throw {
233
- status: 502,
234
- code: 'invalid_profile_response',
235
- error: 'Trackly returned an incomplete profile, so the deletion scope cannot be verified. No changes were saved.',
236
- };
237
- }
238
- const { currentRevision, affectedKeys, confirmationToken } =
239
- sensitiveRevocationConfirmation(profileResponse);
240
- if (sensitiveRevocationConfirmToken !== confirmationToken || body.expectedRevision !== currentRevision) {
241
- throw {
242
- status: 409,
243
- code: 'sensitive_revocation_confirmation_required',
244
- error: 'Revoking sensitive storage consent permanently deletes every stored sensitive and restricted answer. No changes were saved.',
245
- confirmation: {
246
- currentRevision,
247
- affectedKeys,
248
- alsoDeletes: 'Every provider- and company-scoped sensitive or restricted answer is also deleted, along with any stored rows not visible in this profile view; those keys are not listed here.',
249
- confirmationToken,
250
- instructions: 'Do not retry automatically. Show the user the affected keys and get explicit confirmation, then retry with the FULL original request body (including any changes, education, or confirmProfile fields) plus expectedRevision=currentRevision and sensitiveRevocationConfirmToken=confirmationToken. If the original call carried other changes, re-read the profile first and reconcile them against the current revision instead of resending stale changes blindly. The token becomes invalid whenever the profile revision changes.',
251
- },
252
- };
253
- }
254
- }
255
- return apiRequest('PATCH', '/api/jobscout/application-profile', body, false, false, MCP_USER_AGENT);
190
+ // The revocation guard lives in the backend service layer (#1294): it
191
+ // recomputes the persisted-sensitivity inventory in-transaction and 409s
192
+ // with the challenge unless the token matches. Forward the full params —
193
+ // including sensitiveRevocationConfirmToken — so no client-side copy of
194
+ // the guard can drift or be bypassed by version skew.
195
+ return apiRequest('PATCH', '/api/jobscout/application-profile', params, false, false, MCP_USER_AGENT);
256
196
  }, 'Failed to update application profile')
257
197
  );
258
198
 
package/mcp/server.js CHANGED
@@ -99,6 +99,11 @@ function createErrorResult(error, fallbackMessage, extra = {}) {
99
99
  payload.confirmation = error.confirmation;
100
100
  }
101
101
 
102
+ if (error?.status === 409 && error?.error === 'sensitive_revocation_confirmation_required' && error?.confirmation) {
103
+ payload.code = error.error;
104
+ payload.confirmation = error.confirmation;
105
+ }
106
+
102
107
  if (error?.status === 409 && error?.error === 'preference_revision_conflict') {
103
108
  payload.preferences = error.preferences;
104
109
  payload.discoveryPreferenceRevision = error.discoveryPreferenceRevision;
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "trackly-cli",
9
- "version": "0.10.1",
9
+ "version": "0.10.2",
10
10
  "license": "MIT",
11
11
  "dependencies": {
12
12
  "@modelcontextprotocol/sdk": "1.30.0",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trackly-cli",
3
- "version": "0.10.1",
3
+ "version": "0.10.2",
4
4
  "mcpName": "io.github.trackly-app/trackly",
5
5
  "description": "AI job search CLI + hosted MCP server with OAuth. 128K+ jobs, 1,900+ companies, 40+ ATS. Works with Claude, ChatGPT, Cursor, Windsurf, Codex via hosted streamable-http or local stdio.",
6
6
  "bin": {
package/server.json CHANGED
@@ -17,12 +17,12 @@
17
17
  ]
18
18
  }
19
19
  ],
20
- "version": "0.10.1",
20
+ "version": "0.10.2",
21
21
  "packages": [
22
22
  {
23
23
  "registryType": "npm",
24
24
  "identifier": "trackly-cli",
25
- "version": "0.10.1",
25
+ "version": "0.10.2",
26
26
  "runtimeHint": "npx",
27
27
  "runtimeArguments": [
28
28
  {