trackly-cli 0.7.3 → 0.9.0
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/README.md +18 -6
- package/bin/trackly +2 -0
- package/contracts/trackly-apply-tools.json +26 -7
- package/docs/trackly-tools.md +14 -2
- package/lib/agent.js +197 -11
- package/lib/client.js +40 -2
- package/mcp/apply-tools.js +666 -0
- package/mcp/server.js +5 -204
- package/npm-shrinkwrap.json +1177 -0
- package/package.json +12 -4
- package/scripts/verify-audit-exceptions.js +165 -0
- package/security/audit-exceptions.json +4 -0
- package/server.json +2 -2
- package/skills/trackly-apply/SKILL.md +26 -17
- package/skills/trackly-apply/agents/openai.yaml +1 -1
- package/skills/trackly-apply/references/ats-playbook.md +6 -0
- package/skills/trackly-apply/references/batch-orchestration.md +208 -0
- package/skills/trackly-apply/references/browser-lifecycle.md +101 -0
- package/skills/trackly-apply/references/review-handoff.md +53 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[](https://www.npmjs.com/package/trackly-cli)
|
|
2
2
|
[](https://opensource.org/licenses/MIT)
|
|
3
|
-
[](https://nodejs.org/)
|
|
4
4
|
[](https://modelcontextprotocol.io/)
|
|
5
5
|
|
|
6
6
|
# trackly-cli
|
|
@@ -31,11 +31,11 @@ trackly login
|
|
|
31
31
|
trackly jobs --function product
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
-
> **Prerequisites:** [Node.js
|
|
34
|
+
> **Prerequisites:** [Node.js 20+](https://nodejs.org/) (LTS recommended). On macOS with the official `.pkg` installer, global npm installs may require `sudo`.
|
|
35
35
|
|
|
36
36
|
## At a Glance
|
|
37
37
|
|
|
38
|
-
1,900+ companies | 128K+ jobs | 40+ ATS types | CLI + MCP |
|
|
38
|
+
1,900+ companies | 128K+ jobs | 40+ ATS types | CLI + MCP | 36 local MCP tools
|
|
39
39
|
|
|
40
40
|
## CLI Commands
|
|
41
41
|
|
|
@@ -177,11 +177,23 @@ Then use natural language in any of these clients:
|
|
|
177
177
|
| trackly_get_application_profile | Get versioned profile answers and provenance |
|
|
178
178
|
| trackly_get_profile_onboarding | Get schema plus missing questions |
|
|
179
179
|
| trackly_update_application_profile | Save scoped answers with optimistic concurrency |
|
|
180
|
-
|
|
|
180
|
+
| trackly_create_apply_batch | Freeze an exact recent-first approved batch |
|
|
181
|
+
| trackly_get_active_apply_batch | Recover the newest unexpired active batch after context loss |
|
|
182
|
+
| trackly_get_apply_batch | Read frozen membership with opaque pagination |
|
|
183
|
+
| trackly_claim_apply_batch | Acquire or renew a batch mutation lease |
|
|
184
|
+
| trackly_checkpoint_apply_batch | Bulk-record redacted inspection checkpoints and human actions |
|
|
185
|
+
| trackly_bind_apply_surface | Bind an initial or recovered browser surface to the existing run and exact requisition URL |
|
|
186
|
+
| trackly_record_apply_surface_evidence | Record current-epoch inventory, missing-tab, close-receipt, and post-close absence evidence |
|
|
187
|
+
| trackly_record_apply_submission_evidence | Record redacted submit-request, success-page, user-confirmation, or provider-receipt evidence |
|
|
188
|
+
| trackly_approve_apply_batch_resume | Approve one exact default resume for the current frozen run set |
|
|
189
|
+
| trackly_certify_apply_batch_truth | Certify final answer and wording fingerprints after every other review-readiness gate |
|
|
190
|
+
| trackly_start_apply_run | Start or reuse a manual-submit browser run |
|
|
181
191
|
| trackly_get_apply_evidence | Get aggregate, value-free beta evidence and release readiness |
|
|
182
192
|
| trackly_get_apply_protocol | Get current workflow and compatibility rules |
|
|
183
193
|
| trackly_report_apply_observation | Send redacted ATS mechanics feedback |
|
|
194
|
+
| trackly_report_apply_observations | Bulk-send up to 20 leased, batch-bound redacted observations |
|
|
184
195
|
| trackly_record_application_outcome | Record review or confirmed submission outcome |
|
|
196
|
+
| trackly_record_application_outcomes | Bulk-record up to 20 leased batch outcomes with per-member conflicts |
|
|
185
197
|
| trackly_prepare_resume | Prepare a private expiring resume file for upload |
|
|
186
198
|
| trackly_verify_prepared_resume | Recheck the confirmed resume immediately before attachment |
|
|
187
199
|
|
|
@@ -249,7 +261,7 @@ trackly config --base-url http://127.0.0.1:3000 # Point at a different backend
|
|
|
249
261
|
| Job search + filters | Yes | Yes | Yes |
|
|
250
262
|
| Apply/save/dismiss | Yes | Yes | Yes |
|
|
251
263
|
| AI-powered search | Yes (trackly ask) | Yes | Yes |
|
|
252
|
-
| MCP integration | Yes (
|
|
264
|
+
| MCP integration | Yes (36 local tools) | -- | -- |
|
|
253
265
|
| Browser required | No | Yes | No |
|
|
254
266
|
| Best for | Terminal + AI agents | Visual browsing | Custom integrations |
|
|
255
267
|
|
|
@@ -263,7 +275,7 @@ Install trackly-cli (`npm install -g trackly-cli`), authenticate with `trackly l
|
|
|
263
275
|
|
|
264
276
|
**What MCP servers exist for job searching?**
|
|
265
277
|
|
|
266
|
-
trackly-cli includes a built-in MCP server with
|
|
278
|
+
trackly-cli includes a built-in MCP server with 36 tools for job search, company lookup, discovery preferences, application tracking, frozen-batch orchestration, profile onboarding, beta evidence, and manual-submit form preparation. Run `trackly mcp` or use `trackly agent setup --client claude`.
|
|
267
279
|
|
|
268
280
|
**How do I use Claude Code for job hunting?**
|
|
269
281
|
|
package/bin/trackly
CHANGED
|
@@ -1159,6 +1159,8 @@ async function cmdAgent(args) {
|
|
|
1159
1159
|
console.log(color('bold', 'Trackly Apply doctor'));
|
|
1160
1160
|
console.log(`Overall: ${color(report.ok ? 'green' : 'yellow', report.ok ? 'ready' : 'needs attention')}`);
|
|
1161
1161
|
console.log(`Authenticated: ${report.authenticated ? 'yes' : 'no'}`);
|
|
1162
|
+
console.log(`CLI: ${report.cliVersion}; MCP contract: ${report.mcpContractVersion}`);
|
|
1163
|
+
console.log(`Skill: ${report.skillVersion}; digest: ${report.skillPackIntegrity.expectedDigest || 'unavailable'}`);
|
|
1162
1164
|
console.log(`Protocol: ${report.protocol?.version || 'unavailable'}; compatible=${report.compatible ? 'yes' : 'no'}`);
|
|
1163
1165
|
console.log(`Semantic browser + computer use: ${report.browserControl.semanticReady ? 'ready' : 'not verified'}`);
|
|
1164
1166
|
for (const client of report.clients) {
|
|
@@ -1,22 +1,41 @@
|
|
|
1
1
|
{
|
|
2
|
-
"contractVersion": "3.
|
|
2
|
+
"contractVersion": "3.3.1",
|
|
3
3
|
"constants": {
|
|
4
4
|
"applyScenarioCodes": ["browser_reclaim", "resume_upload", "resume_parser_recheck", "semantic_boolean_commit", "custom_select_commit", "multi_step_navigation", "free_text_voice", "required_error_sweep", "final_consent", "handoff_reclaim", "critical_contact_integrity", "manual_submit_boundary", "job_identity_match"],
|
|
5
|
-
"applyBrowserSurfaces": ["codex_in_app", "chrome_extension", "claude_in_chrome"]
|
|
5
|
+
"applyBrowserSurfaces": ["codex_in_app", "chrome_extension", "claude_in_chrome"],
|
|
6
|
+
"applyCheckpointActionCodes": ["answer/unknown", "auth/sign_in", "auth/otp", "captcha/before_form", "captcha/at_submit", "artifact/upload_required", "legal/decision_required", "consent/decision_required", "review/manual_submit", "trust/origin_mismatch", "observability/unverifiable_state"],
|
|
7
|
+
"applyCheckpointPacketPhases": ["first_pass", "delta"],
|
|
8
|
+
"applySurfaceBindingReasons": ["initial_binding", "recovery_binding"],
|
|
9
|
+
"applySurfaceEvidenceTypes": ["surface_inventory_reconciled", "surface_missing", "surface_close_attempted", "surface_close_receipt", "surface_post_close_absent", "surface_close_failed"],
|
|
10
|
+
"applySurfaceOwnershipStates": ["controller_owned", "user_owned", "controller_user_union", "unresolved"],
|
|
11
|
+
"applySubmissionEvidenceTypes": ["request_accepted", "confirmation_detected", "provider_receipt_detected"],
|
|
12
|
+
"applySubmissionEvidenceSources": ["submission_request", "success_page", "user_confirmation", "provider_receipt", "post_submit_receipt"]
|
|
6
13
|
},
|
|
7
14
|
"tools": {
|
|
8
|
-
"trackly_get_apply_queue": "{limit:z.number().int().min(1).max(100).optional()}",
|
|
15
|
+
"trackly_get_apply_queue": "{limit:z.number().int().min(1).max(100).optional(),cursor:z.string().min(1).max(2048).optional()}",
|
|
9
16
|
"trackly_get_application_profile": "{includeSensitive:z.boolean().optional(),provider:z.string().max(100).optional(),companyId:z.string().max(100).optional()}",
|
|
10
17
|
"trackly_get_profile_onboarding": "{}",
|
|
11
18
|
"trackly_update_application_profile": "{expectedRevision:z.number().int().min(1),source:z.enum(['web','ios','macos','codex','claude','mcp']).optional(),changes:z.array(z.discriminatedUnion('scope',[z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('global'),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('provider'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()}),z.object({key:z.string().min(1).max(200),state:z.enum(['unknown','answered','intentionally_blank','declined']),value:z.any().optional(),scope:z.literal('company'),scopeValue:z.string().min(1).max(200),questionLabel:z.string().max(1000).optional()})])).max(100).optional(),education:z.array(z.object({school:z.string().min(1).max(500),degree:z.string().max(500).nullable().optional(),fieldOfStudy:z.string().max(500).nullable().optional(),gpa:z.string().max(50).nullable().optional(),startDate:z.string().max(50).nullable().optional(),endDate:z.string().max(50).nullable().optional()})).max(20).optional(),confirmProfile:z.boolean().optional(),sensitiveStorageConsent:z.boolean().optional()}",
|
|
12
|
-
"
|
|
19
|
+
"trackly_create_apply_batch": "{limit:z.number().int().min(1).max(100),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
20
|
+
"trackly_get_active_apply_batch": "{limit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),cursor:z.string().min(1).max(2048).optional(),actionLimit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),actionCursor:z.string().min(1).max(2048).optional()}",
|
|
21
|
+
"trackly_get_apply_batch": "{batchId:z.number().int().min(1),limit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),cursor:z.string().min(1).max(2048).optional(),actionLimit:z.number().int().min(1).max(APPLY_BATCH_MAX_MEMBERS).optional(),actionCursor:z.string().min(1).max(2048).optional()}",
|
|
22
|
+
"trackly_claim_apply_batch": "{batchId:z.number().int().min(1),expectedRevision:z.number().int().min(1),leaseOwner:z.string().min(1).max(1024),leaseToken:z.string().min(1).max(1024),leaseDurationMs:z.number().int().min(15000).max(300000)}",
|
|
23
|
+
"trackly_checkpoint_apply_batch": "{batchId:z.number().int().min(1),leaseToken:z.string().min(1).max(1024),checkpoints:z.array(z.object({memberId:z.number().int().min(1),runId:z.number().int().min(1),expectedMemberVersion:z.number().int().min(1),expectedInspectionEpoch:z.number().int().min(0),inspectionEpoch:z.number().int().min(0),packetPhase:z.enum(APPLY_CHECKPOINT_PACKET_PHASES).optional(),knownFieldsCommitted:z.boolean(),resolvedActionIds:z.array(z.string().regex(/^[1-9][0-9]*$/)).max(APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT).optional(),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY),actions:z.array(z.object({actionCode:z.enum(APPLY_CHECKPOINT_ACTION_CODES),continuationAllowed:z.boolean(),fieldFingerprint:z.string().regex(/^[a-f0-9]{64}$/).optional()})).min(1).max(APPLY_BATCH_MAX_ACTIONS_PER_CHECKPOINT)})).min(1).max(APPLY_BATCH_MAX_CHECKPOINTS_PER_REQUEST)}",
|
|
24
|
+
"trackly_bind_apply_surface": "{batchId:z.number().int().min(1),memberId:z.number().int().min(1),runId:z.number().int().min(1),expectedMemberVersion:z.number().int().min(1),expectedInspectionEpoch:z.number().int().min(0),leaseToken:z.string().min(1).max(1024),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/),browserSurface:z.enum(APPLY_BROWSER_SURFACES),adapterCode:z.string().regex(SAFE_OBSERVATION_CODE),bindingReason:z.enum(APPLY_SURFACE_BINDING_REASONS),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
25
|
+
"trackly_record_apply_surface_evidence": "{batchId:z.number().int().min(1),memberId:z.number().int().min(1),runId:z.number().int().min(1),expectedMemberVersion:z.number().int().min(1),expectedInspectionEpoch:z.number().int().min(1),leaseToken:z.string().min(1).max(1024),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/),browserSurface:z.enum(APPLY_BROWSER_SURFACES),adapterCode:z.string().regex(SAFE_OBSERVATION_CODE),ownershipState:z.enum(APPLY_SURFACE_OWNERSHIP_STATES),completeInventory:z.boolean(),evidenceType:z.enum(APPLY_SURFACE_EVIDENCE_TYPES),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
26
|
+
"trackly_record_apply_submission_evidence": "{batchId:z.number().int().min(1),memberId:z.number().int().min(1),runId:z.number().int().min(1),expectedMemberVersion:z.number().int().min(1),expectedInspectionEpoch:z.number().int().min(1),leaseToken:z.string().min(1).max(1024),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/),evidenceType:z.enum(APPLY_SUBMISSION_EVIDENCE_TYPES),evidenceSource:z.enum(APPLY_SUBMISSION_EVIDENCE_SOURCES),evidenceFingerprint:z.string().regex(/^[a-f0-9]{64}$/),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
27
|
+
"trackly_approve_apply_batch_resume": "{batchId:z.number().int().min(1),leaseToken:z.string().min(1).max(1024),membershipHash:z.string().regex(/^[a-f0-9]{64}$/),profileRevision:z.number().int().min(0),resumeId:z.number().int().min(1),resumeSha256:z.string().regex(/^[a-f0-9]{64}$/),resumeFilename:z.string().min(1).max(255),resumeSizeBytes:z.number().int().min(1),memberRuns:z.array(z.object({memberId:z.number().int().min(1),runId:z.number().int().min(1),memberVersion:z.number().int().min(1),inspectionEpoch:z.number().int().min(0)})).min(1).max(100),expiresAt:z.string().datetime(),idempotencyKey:z.string().min(16).max(200).regex(SAFE_IDEMPOTENCY_KEY)}",
|
|
28
|
+
"trackly_certify_apply_batch_truth": "truthCertificationSchema",
|
|
29
|
+
"trackly_start_apply_run": "startApplyRunSchema",
|
|
13
30
|
"trackly_get_apply_evidence": "{windowDays:z.number().int().min(1).max(365).optional(),targetReviewedRuns:z.number().int().min(1).max(1000).optional()}",
|
|
14
31
|
"trackly_get_apply_protocol": "{}",
|
|
15
|
-
"trackly_report_apply_observation": "{runId:z.number().int().min(1),provider:z.string().regex(SAFE_OBSERVATION_CODE),fieldLabel:z.string().min(1).max(1000),observationType:z.string().regex(SAFE_OBSERVATION_CODE),resolutionCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),metadata:z.object({controlType:z.string().regex(SAFE_OBSERVATION_CODE).optional(),required:z.boolean().optional(),errorCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),committed:z.boolean(),scenarioCode:z.enum(APPLY_SCENARIO_CODES),browserSurface:z.enum(APPLY_BROWSER_SURFACES),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/).optional(),resumedAfterHandoff:z.boolean().optional()})}",
|
|
16
|
-
"
|
|
32
|
+
"trackly_report_apply_observation": "{runId:z.number().int().min(1),batchId:z.number().int().min(1).optional(),memberId:z.number().int().min(1).optional(),inspectionEpoch:z.number().int().min(0).optional(),leaseToken:z.string().min(1).max(1024).optional(),provider:z.string().regex(SAFE_OBSERVATION_CODE),fieldLabel:z.string().min(1).max(1000),observationType:z.string().regex(SAFE_OBSERVATION_CODE),resolutionCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),metadata:z.object({controlType:z.string().regex(SAFE_OBSERVATION_CODE).optional(),required:z.boolean().optional(),errorCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),committed:z.boolean(),scenarioCode:z.enum(APPLY_SCENARIO_CODES),browserSurface:z.enum(APPLY_BROWSER_SURFACES),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/).optional(),resumedAfterHandoff:z.boolean().optional()})}",
|
|
33
|
+
"trackly_report_apply_observations": "{observations:z.array(z.object({runId:z.number().int().min(1),batchId:z.number().int().min(1),memberId:z.number().int().min(1),inspectionEpoch:z.number().int().min(0),leaseToken:z.string().min(1).max(1024),provider:z.string().regex(SAFE_OBSERVATION_CODE),fieldLabel:z.string().min(1).max(1000),observationType:z.string().regex(SAFE_OBSERVATION_CODE),resolutionCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),metadata:z.object({controlType:z.string().regex(SAFE_OBSERVATION_CODE).optional(),required:z.boolean().optional(),errorCode:z.string().regex(SAFE_OBSERVATION_CODE).optional(),committed:z.boolean(),scenarioCode:z.enum(APPLY_SCENARIO_CODES),browserSurface:z.enum(APPLY_BROWSER_SURFACES),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/).optional(),resumedAfterHandoff:z.boolean().optional()})})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS)}",
|
|
34
|
+
"trackly_record_application_outcome": "{runId:z.number().int().min(1),batchId:z.number().int().min(1).optional(),memberId:z.number().int().min(1).optional(),inspectionEpoch:z.number().int().min(0).optional(),leaseToken:z.string().min(1).max(1024).optional(),outcome:z.enum(['review_ready','submitted','failed','blocked']),confirmation:z.enum(['user_confirmation','success_page']).optional()}",
|
|
35
|
+
"trackly_record_application_outcomes": "{outcomes:z.array(z.object({runId:z.number().int().min(1),batchId:z.number().int().min(1),memberId:z.number().int().min(1),inspectionEpoch:z.number().int().min(0),leaseToken:z.string().min(1).max(1024),outcome:z.enum(['review_ready','submitted','failed','blocked']),confirmation:z.enum(['user_confirmation','success_page']).optional()})).min(1).max(APPLY_BATCH_MAX_BULK_MUTATIONS)}",
|
|
17
36
|
"trackly_prepare_resume": "{runId:z.number().int().min(1),browserSurface:z.enum(APPLY_BROWSER_SURFACES),browserBindingHash:z.string().regex(/^[a-f0-9]{64}$/)}",
|
|
18
37
|
"trackly_verify_prepared_resume": {
|
|
19
|
-
"local": "{runId:z.number().int().min(1),confirmationId:z.string().min(1).max(200),exactLocalPath:z.string().min(1).max(4096),sha256:z.string().regex(/^[a-f0-9]{64}$/i),sizeBytes:z.number().int().min(1),expiresAt:z.string().datetime()}",
|
|
38
|
+
"local": "{runId:z.number().int().min(1),resumeId:z.number().int().min(1),confirmationId:z.string().min(1).max(200),exactLocalPath:z.string().min(1).max(4096),sha256:z.string().regex(/^[a-f0-9]{64}$/i),sizeBytes:z.number().int().min(1),expiresAt:z.string().datetime()}",
|
|
20
39
|
"hosted": "{runId:z.number().int().min(1),confirmationId:z.string().min(1).max(200)}"
|
|
21
40
|
}
|
|
22
41
|
}
|
package/docs/trackly-tools.md
CHANGED
|
@@ -103,15 +103,27 @@ API-key creation attempts.
|
|
|
103
103
|
- **trackly_get_application_profile** — Get the versioned, scoped application profile.
|
|
104
104
|
- **trackly_get_profile_onboarding** — Get backend-owned questions plus only missing/unconfirmed answers.
|
|
105
105
|
- **trackly_update_application_profile** — Save explicit answer states with optimistic concurrency and global/provider/company scope.
|
|
106
|
-
- **
|
|
106
|
+
- **trackly_create_apply_batch** — Freeze an exact recent-first set of approved jobs with an idempotency key.
|
|
107
|
+
- **trackly_get_active_apply_batch** — Recover the newest unexpired active batch before creating another after chat or browser context loss.
|
|
108
|
+
- **trackly_get_apply_batch** — Page a server-owned frozen batch without reordering or replacing members.
|
|
109
|
+
- **trackly_claim_apply_batch** — Acquire or renew the optimistic browser-mutation lease.
|
|
110
|
+
- **trackly_checkpoint_apply_batch** — Bulk-record up to 20 value-free inspection checkpoints, typed human actions, and per-member conflicts.
|
|
111
|
+
- **trackly_bind_apply_surface** — Bind an initial or recovered browser surface to the existing frozen member/run, increment its inspection epoch, and return the exact stored requisition URL.
|
|
112
|
+
- **trackly_record_apply_surface_evidence** — Record current-epoch, value-free inventory, missing-tab, close-receipt, post-close absence, or close-failure evidence.
|
|
113
|
+
- **trackly_record_apply_submission_evidence** — Record typed, redacted submit-request, success-page, explicit user-confirmation, or provider-receipt evidence without page text or external references.
|
|
114
|
+
- **trackly_approve_apply_batch_resume** — Approve one exact default-resume identity and content hash for the complete current frozen run set.
|
|
115
|
+
- **trackly_certify_apply_batch_truth** — Record an expiring late truthfulness certification over final answer and wording fingerprints after every other review-readiness gate has passed.
|
|
116
|
+
- **trackly_start_apply_run** — Start or reuse a manual-submit browser run with the complete frozen batch/member/lease binding.
|
|
107
117
|
- **trackly_get_apply_evidence** — Get the authenticated user's aggregate beta evidence and release gate without returning profile answers or contact values.
|
|
108
118
|
- **trackly_get_apply_protocol** — Get the current ATS support, browser integrity rules, and compatible skill version.
|
|
109
119
|
- **trackly_report_apply_observation** — Report redacted ATS mechanics and actual scenario coverage without answer values or page text.
|
|
120
|
+
- **trackly_report_apply_observations** — Bulk-report up to 20 leased, current-epoch, batch-bound redacted observations in one request.
|
|
110
121
|
- **trackly_record_application_outcome** — Record review readiness or a confirmed manual submission.
|
|
122
|
+
- **trackly_record_application_outcomes** — Bulk-record up to 20 leased, batch-bound outcomes while returning per-member conflicts explicitly.
|
|
111
123
|
- **trackly_prepare_resume** — Local MCP only: materialize the default resume in a private, expiring mode-0600 cache and return filename, size, SHA-256, exact local path, and visual-confirmation metadata. Hosted MCP returns a manual/local-agent requirement.
|
|
112
124
|
- **trackly_verify_prepared_resume** — Local MCP only: immediately before attachment, recompute the user-confirmed resume hash and size, validate the exact path/run/expiration, and lock the file read-only. Any mismatch requires a fresh preview and confirmation.
|
|
113
125
|
|
|
114
|
-
Apply contract v3 intentionally gives this verifier different local and hosted schemas: local MCP receives the full proof needed to inspect the private file, while hosted MCP accepts only run and confirmation identifiers and returns the manual/local-agent requirement. Local paths
|
|
126
|
+
Apply contract v3 intentionally gives this verifier different local and hosted schemas: local MCP receives the full proof needed to inspect the private file, while hosted MCP accepts only run and confirmation identifiers and returns the manual/local-agent requirement. Local paths are never sent remotely. Resume fingerprints are sent only to authenticated Trackly resume approval and truth-certification endpoints, never observations or employer forms. Version 3.1 also records universal value-free evidence for critical-contact integrity and the manual-submit boundary. Version 3.2 authorizes the exact stored HTTPS origin for jobs Trackly ingested from employer careers sources, without granting redirect, iframe, or hostname-suffix privileges. Version 3.3.1 adds active-batch recovery, epoch-bound observations/outcomes, and truth certification for forms with no resume control.
|
|
115
127
|
|
|
116
128
|
### Maintenance behavior
|
|
117
129
|
|
package/lib/agent.js
CHANGED
|
@@ -7,15 +7,17 @@ const path = require('node:path');
|
|
|
7
7
|
const { spawnSync } = require('node:child_process');
|
|
8
8
|
const { apiRequest, downloadFile, getConfigPaths, hasAuth, maintenanceOutput } = require('./client');
|
|
9
9
|
const { version: PACKAGE_VERSION } = require('../package.json');
|
|
10
|
-
const
|
|
10
|
+
const { contractVersion: MCP_CONTRACT_VERSION } = require('../contracts/trackly-apply-tools.json');
|
|
11
|
+
const SKILL_VERSION = '4.2.2';
|
|
11
12
|
const CLI_USER_AGENT = `trackly-cli/${PACKAGE_VERSION}`;
|
|
12
13
|
const MCP_USER_AGENT = `trackly-mcp/${PACKAGE_VERSION}`;
|
|
13
14
|
|
|
14
15
|
const SKILL_NAME = 'trackly-apply';
|
|
15
16
|
const SKILL_MAJOR = Number(SKILL_VERSION.split('.')[0]);
|
|
16
|
-
const MIN_APPLY_PROTOCOL_VERSION = '3.1
|
|
17
|
+
const MIN_APPLY_PROTOCOL_VERSION = '3.3.1';
|
|
17
18
|
const CACHE_TTL_MS = 2 * 60 * 60 * 1000;
|
|
18
19
|
const MANAGED_FILE = '.trackly-managed.json';
|
|
20
|
+
const SKILL_MANIFEST_VERSION = 1;
|
|
19
21
|
const RESUME_PROOF_FILE = '.trackly-resume-proof.json';
|
|
20
22
|
const RESUME_PROOF_KEY_BYTES = 32;
|
|
21
23
|
|
|
@@ -52,6 +54,43 @@ function copyDirectory(source, destination) {
|
|
|
52
54
|
}
|
|
53
55
|
}
|
|
54
56
|
|
|
57
|
+
function skillContentManifest(root) {
|
|
58
|
+
const files = [];
|
|
59
|
+
|
|
60
|
+
function collect(directory, relativeDirectory = '') {
|
|
61
|
+
const names = fs.readdirSync(directory).sort();
|
|
62
|
+
for (const name of names) {
|
|
63
|
+
if (!relativeDirectory && name === MANAGED_FILE) continue;
|
|
64
|
+
const absolutePath = path.join(directory, name);
|
|
65
|
+
const relativePath = relativeDirectory ? `${relativeDirectory}/${name}` : name;
|
|
66
|
+
const stat = fs.lstatSync(absolutePath);
|
|
67
|
+
if (stat.isDirectory()) {
|
|
68
|
+
collect(absolutePath, relativePath);
|
|
69
|
+
continue;
|
|
70
|
+
}
|
|
71
|
+
if (!stat.isFile()) {
|
|
72
|
+
throw new Error(`Trackly Apply skill contains an unsupported filesystem entry: ${relativePath}`);
|
|
73
|
+
}
|
|
74
|
+
const content = fs.readFileSync(absolutePath);
|
|
75
|
+
files.push({
|
|
76
|
+
path: relativePath,
|
|
77
|
+
sizeBytes: content.length,
|
|
78
|
+
sha256: crypto.createHash('sha256').update(content).digest('hex'),
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
collect(root);
|
|
84
|
+
const digest = crypto.createHash('sha256')
|
|
85
|
+
.update(JSON.stringify({ version: SKILL_MANIFEST_VERSION, files }))
|
|
86
|
+
.digest('hex');
|
|
87
|
+
return {
|
|
88
|
+
version: SKILL_MANIFEST_VERSION,
|
|
89
|
+
digest,
|
|
90
|
+
fileCount: files.length,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
55
94
|
function managedMetadata(extra = {}) {
|
|
56
95
|
return JSON.stringify({ managedBy: 'trackly-cli', skill: SKILL_NAME, skillVersion: SKILL_VERSION, ...extra }, null, 2);
|
|
57
96
|
}
|
|
@@ -80,7 +119,12 @@ function installCanonicalSkill() {
|
|
|
80
119
|
const staging = `${destination}.staging.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
|
|
81
120
|
try {
|
|
82
121
|
copyDirectory(source, staging);
|
|
83
|
-
|
|
122
|
+
const manifest = skillContentManifest(staging);
|
|
123
|
+
fs.writeFileSync(path.join(staging, MANAGED_FILE), managedMetadata({
|
|
124
|
+
skillManifestVersion: manifest.version,
|
|
125
|
+
skillContentDigest: manifest.digest,
|
|
126
|
+
skillFileCount: manifest.fileCount,
|
|
127
|
+
}), { mode: 0o600 });
|
|
84
128
|
} catch (error) {
|
|
85
129
|
if (fs.existsSync(staging)) fs.rmSync(staging, { recursive: true, force: true });
|
|
86
130
|
throw error;
|
|
@@ -157,7 +201,13 @@ function linkClientSkill(client, canonical) {
|
|
|
157
201
|
const staging = `${target}.staging.${process.pid}.${crypto.randomBytes(4).toString('hex')}`;
|
|
158
202
|
try {
|
|
159
203
|
copyDirectory(canonical, staging);
|
|
160
|
-
|
|
204
|
+
const manifest = skillContentManifest(staging);
|
|
205
|
+
fs.writeFileSync(path.join(staging, MANAGED_FILE), managedMetadata({
|
|
206
|
+
copiedFrom: canonical,
|
|
207
|
+
skillManifestVersion: manifest.version,
|
|
208
|
+
skillContentDigest: manifest.digest,
|
|
209
|
+
skillFileCount: manifest.fileCount,
|
|
210
|
+
}), { mode: 0o600 });
|
|
161
211
|
fs.renameSync(staging, target);
|
|
162
212
|
if (backup) fs.rmSync(backup, { recursive: true, force: true });
|
|
163
213
|
return { client, target, method: 'copy', status: 'installed' };
|
|
@@ -281,10 +331,47 @@ function inspectClient(client) {
|
|
|
281
331
|
const target = clientSkillDir(client);
|
|
282
332
|
const executable = commandExists(client === 'codex' ? 'codex' : 'claude');
|
|
283
333
|
const metadata = managedSkillMetadata(target);
|
|
284
|
-
|
|
334
|
+
let expectedManifest = null;
|
|
335
|
+
let installedManifest = null;
|
|
336
|
+
try {
|
|
337
|
+
expectedManifest = skillContentManifest(bundledSkillDir());
|
|
338
|
+
installedManifest = skillContentManifest(target);
|
|
339
|
+
} catch (_) {}
|
|
340
|
+
const hasRecordedManifest = metadata?.skillManifestVersion === SKILL_MANIFEST_VERSION
|
|
341
|
+
&& typeof metadata?.skillContentDigest === 'string'
|
|
342
|
+
&& Number.isInteger(metadata?.skillFileCount);
|
|
343
|
+
const contentMatches = hasRecordedManifest
|
|
344
|
+
&& expectedManifest !== null
|
|
345
|
+
&& installedManifest !== null
|
|
346
|
+
&& metadata.skillContentDigest === expectedManifest.digest
|
|
347
|
+
&& metadata.skillContentDigest === installedManifest.digest
|
|
348
|
+
&& metadata.skillFileCount === expectedManifest.fileCount
|
|
349
|
+
&& metadata.skillFileCount === installedManifest.fileCount;
|
|
350
|
+
const skillIntegrity = contentMatches
|
|
351
|
+
? 'verified'
|
|
352
|
+
: metadata && !hasRecordedManifest
|
|
353
|
+
? 'manifest_missing'
|
|
354
|
+
: metadata
|
|
355
|
+
? 'content_mismatch'
|
|
356
|
+
: 'not_managed';
|
|
357
|
+
const installed = fs.existsSync(path.join(target, 'SKILL.md'))
|
|
358
|
+
&& metadata?.skillVersion === SKILL_VERSION
|
|
359
|
+
&& skillIntegrity === 'verified';
|
|
285
360
|
const mcpRegistration = mcpRegistrationState(client);
|
|
286
361
|
const mcpRegistered = mcpRegistration === 'current';
|
|
287
|
-
return {
|
|
362
|
+
return {
|
|
363
|
+
client,
|
|
364
|
+
executable,
|
|
365
|
+
installed,
|
|
366
|
+
installedSkillVersion: metadata?.skillVersion || null,
|
|
367
|
+
skillIntegrity,
|
|
368
|
+
expectedSkillContentDigest: expectedManifest?.digest || null,
|
|
369
|
+
installedSkillContentDigest: installedManifest?.digest || null,
|
|
370
|
+
recordedSkillContentDigest: metadata?.skillContentDigest || null,
|
|
371
|
+
mcpRegistered,
|
|
372
|
+
mcpRegistration,
|
|
373
|
+
target,
|
|
374
|
+
};
|
|
288
375
|
}
|
|
289
376
|
|
|
290
377
|
function cacheDir() {
|
|
@@ -368,11 +455,21 @@ function resumeCacheName(fileName, contentType, now = Date.now(), nonce = crypto
|
|
|
368
455
|
return path.join(`${now}-${nonce}`, safeResumeName(fileName, contentType));
|
|
369
456
|
}
|
|
370
457
|
|
|
371
|
-
function resumeConfirmation(
|
|
458
|
+
function resumeConfirmation(
|
|
459
|
+
fileName,
|
|
460
|
+
sha256,
|
|
461
|
+
sizeBytes,
|
|
462
|
+
exactLocalPath,
|
|
463
|
+
runId,
|
|
464
|
+
expiresAt,
|
|
465
|
+
confirmationId,
|
|
466
|
+
resumeId = null,
|
|
467
|
+
) {
|
|
372
468
|
return {
|
|
373
469
|
required: true,
|
|
374
470
|
source: 'trackly_default_resume',
|
|
375
471
|
runId,
|
|
472
|
+
...(resumeId == null ? {} : { resumeId }),
|
|
376
473
|
confirmationId,
|
|
377
474
|
fileName,
|
|
378
475
|
sha256,
|
|
@@ -450,6 +547,10 @@ function verifyResumeProofSignature(manifest) {
|
|
|
450
547
|
function materializeResume(download, options = {}) {
|
|
451
548
|
const runId = Number(options.runId);
|
|
452
549
|
if (!Number.isInteger(runId) || runId < 1) throw new Error('A valid Trackly Apply run ID is required before preparing a resume.');
|
|
550
|
+
const resumeId = download.resumeId == null ? null : Number(download.resumeId);
|
|
551
|
+
if (resumeId !== null && (!Number.isInteger(resumeId) || resumeId < 1)) {
|
|
552
|
+
throw new Error('Trackly returned an invalid default resume identity.');
|
|
553
|
+
}
|
|
453
554
|
const now = options.now ?? Date.now();
|
|
454
555
|
const nonce = options.nonce ?? crypto.randomBytes(4).toString('hex');
|
|
455
556
|
const confirmationId = options.confirmationId ?? crypto.randomUUID();
|
|
@@ -471,11 +572,13 @@ function materializeResume(download, options = {}) {
|
|
|
471
572
|
runId,
|
|
472
573
|
expiresAt,
|
|
473
574
|
confirmationId,
|
|
575
|
+
resumeId,
|
|
474
576
|
);
|
|
475
577
|
const manifestPath = path.join(path.dirname(destination), RESUME_PROOF_FILE);
|
|
476
578
|
try {
|
|
477
579
|
const manifest = signedResumeProof({
|
|
478
580
|
runId,
|
|
581
|
+
resumeId,
|
|
479
582
|
confirmationId,
|
|
480
583
|
exactLocalPath: destination,
|
|
481
584
|
fileName,
|
|
@@ -494,6 +597,7 @@ function materializeResume(download, options = {}) {
|
|
|
494
597
|
}
|
|
495
598
|
return {
|
|
496
599
|
path: destination,
|
|
600
|
+
resumeId,
|
|
497
601
|
fileName,
|
|
498
602
|
contentType: validated.contentType,
|
|
499
603
|
sha256: validated.digest,
|
|
@@ -542,6 +646,11 @@ function verifyPreparedResume(proof, now = Date.now()) {
|
|
|
542
646
|
}
|
|
543
647
|
|
|
544
648
|
const bindingMatches = manifest.runId === runId
|
|
649
|
+
&& (
|
|
650
|
+
proof.resumeId == null
|
|
651
|
+
? manifest.resumeId == null
|
|
652
|
+
: manifest.resumeId === Number(proof.resumeId)
|
|
653
|
+
)
|
|
545
654
|
&& manifest.confirmationId === proof.confirmationId
|
|
546
655
|
&& manifest.exactLocalPath === expectedPath
|
|
547
656
|
&& manifest.fileName === path.basename(expectedPath)
|
|
@@ -570,6 +679,7 @@ function verifyPreparedResume(proof, now = Date.now()) {
|
|
|
570
679
|
return {
|
|
571
680
|
verified: true,
|
|
572
681
|
runId,
|
|
682
|
+
resumeId: manifest.resumeId ?? null,
|
|
573
683
|
confirmationId: proof.confirmationId,
|
|
574
684
|
exactLocalPath: expectedPath,
|
|
575
685
|
sha256,
|
|
@@ -621,6 +731,9 @@ async function prepareResume(runId, browserSurface, browserBindingHash, userAgen
|
|
|
621
731
|
if (Number(download.applyRunId) !== normalizedRunId) {
|
|
622
732
|
throw new Error('Trackly did not confirm that the resume belongs to the active Apply run.');
|
|
623
733
|
}
|
|
734
|
+
if (!Number.isInteger(Number(download.resumeId)) || Number(download.resumeId) < 1) {
|
|
735
|
+
throw new Error('Trackly did not confirm the default resume identity.');
|
|
736
|
+
}
|
|
624
737
|
return materializeResume(download, { runId: normalizedRunId });
|
|
625
738
|
} finally {
|
|
626
739
|
download.buffer.fill(0);
|
|
@@ -631,7 +744,12 @@ function protocolAtLeast(version, minimum = MIN_APPLY_PROTOCOL_VERSION) {
|
|
|
631
744
|
const parse = (value) => String(value || '').split('.').map((part) => Number(part));
|
|
632
745
|
const actual = parse(version);
|
|
633
746
|
const required = parse(minimum);
|
|
634
|
-
if (
|
|
747
|
+
if (
|
|
748
|
+
actual.length < 2
|
|
749
|
+
|| required.length < 2
|
|
750
|
+
|| actual.some((part) => !Number.isInteger(part) || part < 0)
|
|
751
|
+
|| required.some((part) => !Number.isInteger(part) || part < 0)
|
|
752
|
+
) return false;
|
|
635
753
|
for (let index = 0; index < Math.max(actual.length, required.length); index++) {
|
|
636
754
|
const left = actual[index] || 0;
|
|
637
755
|
const right = required[index] || 0;
|
|
@@ -640,6 +758,47 @@ function protocolAtLeast(version, minimum = MIN_APPLY_PROTOCOL_VERSION) {
|
|
|
640
758
|
return true;
|
|
641
759
|
}
|
|
642
760
|
|
|
761
|
+
function evaluateApplyCompatibility(protocol, clients) {
|
|
762
|
+
const compatibleSkillMajor = Number(protocol?.compatibleSkillMajor);
|
|
763
|
+
const minimumSkillVersion = typeof protocol?.compatibleSkillMinimumVersion === 'string'
|
|
764
|
+
? protocol.compatibleSkillMinimumVersion
|
|
765
|
+
: null;
|
|
766
|
+
const minimumCliVersion = typeof protocol?.compatibleCliMinimumVersion === 'string'
|
|
767
|
+
? protocol.compatibleCliMinimumVersion
|
|
768
|
+
: null;
|
|
769
|
+
const mcpContractCompatible = protocol?.mcpContractVersion === MCP_CONTRACT_VERSION;
|
|
770
|
+
const cliMinimumSatisfied = minimumCliVersion
|
|
771
|
+
? protocolAtLeast(PACKAGE_VERSION, minimumCliVersion)
|
|
772
|
+
: false;
|
|
773
|
+
const protocolCompatible = compatibleSkillMajor === SKILL_MAJOR
|
|
774
|
+
&& protocolAtLeast(protocol?.version, MIN_APPLY_PROTOCOL_VERSION);
|
|
775
|
+
const compatibleClientSkills = minimumSkillVersion
|
|
776
|
+
? clients
|
|
777
|
+
.filter((client) => {
|
|
778
|
+
const installedMajor = Number(String(client.installedSkillVersion || '').split('.')[0]);
|
|
779
|
+
return client.installed === true
|
|
780
|
+
&& installedMajor === compatibleSkillMajor
|
|
781
|
+
&& protocolAtLeast(client.installedSkillVersion, minimumSkillVersion);
|
|
782
|
+
})
|
|
783
|
+
.map((client) => client.client)
|
|
784
|
+
: [];
|
|
785
|
+
const skillMinimumSatisfied = compatibleClientSkills.length > 0;
|
|
786
|
+
return {
|
|
787
|
+
protocolCompatible,
|
|
788
|
+
mcpContractVersion: MCP_CONTRACT_VERSION,
|
|
789
|
+
mcpContractCompatible,
|
|
790
|
+
minimumCliVersion,
|
|
791
|
+
cliMinimumSatisfied,
|
|
792
|
+
minimumSkillVersion,
|
|
793
|
+
compatibleClientSkills,
|
|
794
|
+
skillMinimumSatisfied,
|
|
795
|
+
compatible: protocolCompatible
|
|
796
|
+
&& mcpContractCompatible
|
|
797
|
+
&& cliMinimumSatisfied
|
|
798
|
+
&& skillMinimumSatisfied,
|
|
799
|
+
};
|
|
800
|
+
}
|
|
801
|
+
|
|
643
802
|
function liveBrowserReady(browserControl) {
|
|
644
803
|
return (browserControl?.codex === true && browserControl?.codexComputerUse === true)
|
|
645
804
|
|| browserControl?.claude === true;
|
|
@@ -654,13 +813,38 @@ async function doctorAgent() {
|
|
|
654
813
|
codexComputerUse,
|
|
655
814
|
claude: null,
|
|
656
815
|
};
|
|
816
|
+
const tabInventory = {
|
|
817
|
+
controllerOwnedTabs: 'runtime_verification_required',
|
|
818
|
+
userOwnedTabs: 'runtime_verification_required',
|
|
819
|
+
unionReconciliation: 'runtime_verification_required',
|
|
820
|
+
detectedSurfaces: [],
|
|
821
|
+
unionInventoryDetected: false,
|
|
822
|
+
requiredForClosedClaim: true,
|
|
823
|
+
note: 'Installed plugins do not prove controller-owned and user-owned tab union inventory. The active browser client must verify both inventories before claiming a tab is closed.',
|
|
824
|
+
};
|
|
825
|
+
const clients = ['codex', 'claude'].map(inspectClient);
|
|
826
|
+
const skillIntegrityFailures = clients
|
|
827
|
+
.filter((client) => (
|
|
828
|
+
client.installedSkillVersion === SKILL_VERSION
|
|
829
|
+
&& client.skillIntegrity !== 'verified'
|
|
830
|
+
))
|
|
831
|
+
.map((client) => ({ client: client.client, integrity: client.skillIntegrity }));
|
|
657
832
|
const report = {
|
|
833
|
+
cliVersion: PACKAGE_VERSION,
|
|
834
|
+
mcpContractVersion: MCP_CONTRACT_VERSION,
|
|
658
835
|
skillVersion: SKILL_VERSION,
|
|
659
836
|
authenticated: hasAuth(),
|
|
660
|
-
clients
|
|
837
|
+
clients,
|
|
838
|
+
skillPackIntegrity: {
|
|
839
|
+
ok: skillIntegrityFailures.length === 0,
|
|
840
|
+
expectedDigest: clients.find((client) => client.expectedSkillContentDigest)
|
|
841
|
+
?.expectedSkillContentDigest || null,
|
|
842
|
+
failures: skillIntegrityFailures,
|
|
843
|
+
},
|
|
661
844
|
browserControl: {
|
|
662
845
|
...browserControl,
|
|
663
846
|
semanticReady: liveBrowserReady(browserControl),
|
|
847
|
+
tabInventory,
|
|
664
848
|
note: 'A live run requires both semantic browser control and computer use. Claude capability must be verified by the active client before the run starts.',
|
|
665
849
|
},
|
|
666
850
|
cache,
|
|
@@ -682,8 +866,8 @@ async function doctorAgent() {
|
|
|
682
866
|
completeness: profile.profile?.completeness,
|
|
683
867
|
hasDefaultResume: Boolean(profile.profile?.defaultResume),
|
|
684
868
|
};
|
|
685
|
-
report.
|
|
686
|
-
|
|
869
|
+
report.compatibility = evaluateApplyCompatibility(report.protocol, report.clients);
|
|
870
|
+
report.compatible = report.compatibility.compatible;
|
|
687
871
|
if (report.profile.hasDefaultResume) {
|
|
688
872
|
report.resume = {
|
|
689
873
|
available: true,
|
|
@@ -697,6 +881,7 @@ async function doctorAgent() {
|
|
|
697
881
|
report.ok = report.authenticated
|
|
698
882
|
&& report.compatible
|
|
699
883
|
&& report.browserControl.semanticReady
|
|
884
|
+
&& report.skillPackIntegrity.ok
|
|
700
885
|
&& report.clients.some((client) => client.executable && client.installed && client.mcpRegistered)
|
|
701
886
|
&& Boolean(report.profile?.hasDefaultResume)
|
|
702
887
|
&& report.resume.available
|
|
@@ -724,6 +909,7 @@ module.exports = {
|
|
|
724
909
|
cleanResumeCache,
|
|
725
910
|
clientSkillDir,
|
|
726
911
|
doctorAgent,
|
|
912
|
+
evaluateApplyCompatibility,
|
|
727
913
|
inspectClient,
|
|
728
914
|
liveBrowserReady,
|
|
729
915
|
materializeResume,
|
package/lib/client.js
CHANGED
|
@@ -509,7 +509,15 @@ async function _doRefresh() {
|
|
|
509
509
|
return null;
|
|
510
510
|
}
|
|
511
511
|
|
|
512
|
-
function apiRequest(
|
|
512
|
+
function apiRequest(
|
|
513
|
+
method,
|
|
514
|
+
endpoint,
|
|
515
|
+
data = null,
|
|
516
|
+
skipAuth = false,
|
|
517
|
+
_isRetry = false,
|
|
518
|
+
userAgent = null,
|
|
519
|
+
additionalHeaders = null
|
|
520
|
+
) {
|
|
513
521
|
return new Promise((resolve, reject) => {
|
|
514
522
|
let url;
|
|
515
523
|
try {
|
|
@@ -522,6 +530,27 @@ function apiRequest(method, endpoint, data = null, skipAuth = false, _isRetry =
|
|
|
522
530
|
const isHttps = url.protocol === 'https:';
|
|
523
531
|
const lib = isHttps ? https : http;
|
|
524
532
|
const headers = getRequestHeaders(skipAuth, userAgent);
|
|
533
|
+
if (additionalHeaders !== null) {
|
|
534
|
+
const entries = (
|
|
535
|
+
typeof additionalHeaders === 'object'
|
|
536
|
+
&& additionalHeaders !== null
|
|
537
|
+
&& !Array.isArray(additionalHeaders)
|
|
538
|
+
&& Object.getPrototypeOf(additionalHeaders) === Object.prototype
|
|
539
|
+
)
|
|
540
|
+
? Object.entries(additionalHeaders)
|
|
541
|
+
: [];
|
|
542
|
+
const idempotencyEntry = entries.find(([name]) => name.toLowerCase() === 'idempotency-key');
|
|
543
|
+
if (
|
|
544
|
+
entries.length !== 1
|
|
545
|
+
|| !idempotencyEntry
|
|
546
|
+
|| typeof idempotencyEntry[1] !== 'string'
|
|
547
|
+
|| !/^[\x20-\x7e]{16,200}$/.test(idempotencyEntry[1])
|
|
548
|
+
) {
|
|
549
|
+
reject(new Error('Only a valid Idempotency-Key additional header is permitted.'));
|
|
550
|
+
return;
|
|
551
|
+
}
|
|
552
|
+
headers['Idempotency-Key'] = idempotencyEntry[1];
|
|
553
|
+
}
|
|
525
554
|
const authMethod = getAuthMethod();
|
|
526
555
|
const timeoutMs = getRequestTimeoutMs();
|
|
527
556
|
const hasSensitivePayload = Boolean(data && typeof data === 'object' && 'refreshToken' in data);
|
|
@@ -604,7 +633,15 @@ function apiRequest(method, endpoint, data = null, skipAuth = false, _isRetry =
|
|
|
604
633
|
}
|
|
605
634
|
if (newToken) {
|
|
606
635
|
try {
|
|
607
|
-
const retried = await apiRequest(
|
|
636
|
+
const retried = await apiRequest(
|
|
637
|
+
method,
|
|
638
|
+
endpoint,
|
|
639
|
+
data,
|
|
640
|
+
false,
|
|
641
|
+
true,
|
|
642
|
+
userAgent,
|
|
643
|
+
additionalHeaders
|
|
644
|
+
);
|
|
608
645
|
return resolve(retried);
|
|
609
646
|
} catch (e) { return reject(e); }
|
|
610
647
|
}
|
|
@@ -730,6 +767,7 @@ function downloadFile(endpoint, userAgent = null, _isRetry = false) {
|
|
|
730
767
|
fileName: contentDispositionFileName(disposition),
|
|
731
768
|
sha256: res.headers['x-trackly-file-sha256'] || null,
|
|
732
769
|
applyRunId: res.headers['x-trackly-apply-run-id'] || null,
|
|
770
|
+
resumeId: res.headers['x-trackly-resume-id'] || null,
|
|
733
771
|
});
|
|
734
772
|
});
|
|
735
773
|
});
|