trackly-cli 0.17.0 → 0.17.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.
@@ -1,6 +1,7 @@
1
1
  #!/usr/bin/env node
2
2
  'use strict';
3
3
 
4
+ const { createHash } = require('node:crypto');
4
5
  const { StringDecoder } = require('node:string_decoder');
5
6
 
6
7
  const ACCESS_STATES = new Set([
@@ -24,6 +25,79 @@ const TAB_STATES = new Set(['open', 'missing', 'closure_unverified', 'closed_ver
24
25
  const MAX_RECEIPT_BYTES = 64 * 1024;
25
26
  const WORK_MODES = new Set(['accessible_execution', 'fixed_inspection']);
26
27
  const COMMON_LINEAGE_FIELDS = ['workMode', 'batchId', 'memberId', 'jobId', 'runId', 'inspectionEpoch'];
28
+ const SHA256_PATTERN = /^[a-f0-9]{64}$/;
29
+ const COMMITTED_CONTROL_FIELDS = [
30
+ 'filledExactProfile',
31
+ 'filledSafeDerivation',
32
+ 'filledSupportedDraft',
33
+ 'preservedUserEdit',
34
+ ];
35
+ const EXCEPTION_CONTROL_FIELDS = [
36
+ 'missingFact',
37
+ 'liveConsent',
38
+ 'authenticationBlocker',
39
+ 'unobservableCommit',
40
+ 'unsupportedControl',
41
+ 'notApplicable',
42
+ ];
43
+ const CONTROL_ACCOUNTING_FIELDS = [
44
+ ...COMMITTED_CONTROL_FIELDS,
45
+ ...EXCEPTION_CONTROL_FIELDS,
46
+ ];
47
+ const ANSWER_SCOPE_FIELDS = ['run', 'question', 'office', 'jurisdiction', 'company', 'provider', 'global'];
48
+ const HISTORY_FIELDS = [
49
+ 'canonicalEducationRecordCount',
50
+ 'accountedEducationRecordCount',
51
+ 'canonicalEmploymentPositionCount',
52
+ 'accountedEmploymentPositionCount',
53
+ 'educationOrderVerified',
54
+ 'employmentOrderVerified',
55
+ 'datePrecisionInvented',
56
+ 'educationReconciliationFingerprint',
57
+ 'employmentReconciliationFingerprint',
58
+ ];
59
+ const RESUME_AUDIT_FIELDS = [
60
+ 'control',
61
+ 'mode',
62
+ 'approval',
63
+ 'preAttachVerification',
64
+ 'attachmentCommit',
65
+ 'filenameVerification',
66
+ 'parserRecheck',
67
+ 'finalSweep',
68
+ ];
69
+ const EMPLOYER_APPLICATION_STATES = new Set([
70
+ 'not_opened',
71
+ 'access_probed',
72
+ 'form_reached',
73
+ 'partially_filled',
74
+ 'needs_answers',
75
+ 'review_state_prepared',
76
+ 'manually_submitted',
77
+ 'ats_success_observed',
78
+ 'blocked',
79
+ ]);
80
+ const TRACKLY_MEMBER_STATES = new Set([
81
+ 'reserved',
82
+ 'inspecting',
83
+ 'needs_input',
84
+ 'review_ready',
85
+ 'awaiting_manual_submit',
86
+ 'submitted',
87
+ 'blocked',
88
+ 'parked',
89
+ 'stopped',
90
+ ]);
91
+ const TRACKLY_JOB_STATES = new Set(['new', 'check_later', 'not_interested', 'applied_confirmed', 'unknown']);
92
+ const BROWSER_HANDOFF_STATES = new Set([
93
+ 'no_known_tab',
94
+ 'controller_owned',
95
+ 'user_inventory',
96
+ 'visible',
97
+ 'durable_handoff_proven',
98
+ 'missing',
99
+ 'closure_unverified',
100
+ ]);
27
101
 
28
102
  const PHASE_FIELDS = {
29
103
  selection: new Set([
@@ -59,13 +133,21 @@ const PHASE_FIELDS = {
59
133
  'jobId',
60
134
  'runId',
61
135
  'inspectionEpoch',
136
+ 'profileRevision',
62
137
  'visibleControlCount',
63
138
  'committedControlCount',
64
139
  'typedExceptionCount',
140
+ 'controlAccounting',
141
+ 'formInventoryFingerprint',
65
142
  'knownOmissionCount',
66
143
  'knownFieldsFilledBeforeQuestions',
144
+ 'answerLookupCompleted',
145
+ 'answerLookupScopeCounts',
146
+ 'answerLookupFingerprint',
67
147
  'parserSensitiveFieldsRechecked',
68
148
  'educationAndEmploymentVerified',
149
+ 'historyReconciliation',
150
+ 'resumeAudit',
69
151
  'writingPresent',
70
152
  'localWritingGate',
71
153
  'humanizerAvailability',
@@ -81,11 +163,56 @@ const PHASE_FIELDS = {
81
163
  'jobId',
82
164
  'runId',
83
165
  'inspectionEpoch',
166
+ 'profileRevision',
167
+ 'formInventoryFingerprint',
84
168
  'finalIntegrityPassed',
85
169
  'truthConfirmationRecorded',
170
+ 'truthCertificationAttestationId',
171
+ 'truthCertificationDependencyHash',
172
+ 'truthCertificationExpiresAt',
173
+ 'truthCertificationMemberRuns',
174
+ 'truthCertificationRunSetHash',
175
+ 'truthCertificationStatus',
86
176
  'submitActivated',
87
177
  'reviewTabPreserved',
88
178
  'userVisibleHandoffProven',
179
+ 'browserBindingHash',
180
+ 'handoffEvidenceFingerprint',
181
+ 'handoffEvidenceType',
182
+ 'checkpointAction',
183
+ 'continuationAllowed',
184
+ 'resolvedActionCount',
185
+ 'resolvedActionIdsFingerprint',
186
+ 'checkpointStatus',
187
+ 'checkpointMemberVersion',
188
+ 'checkpointInspectionEpoch',
189
+ 'checkpointLifecycle',
190
+ 'checkpointActionCount',
191
+ 'checkpointActionIdsFingerprint',
192
+ ]),
193
+ handoff: new Set([
194
+ 'workMode',
195
+ 'executionId',
196
+ 'batchId',
197
+ 'memberId',
198
+ 'jobId',
199
+ 'runId',
200
+ 'inspectionEpoch',
201
+ 'employerApplicationState',
202
+ 'tracklyMemberState',
203
+ 'tracklyJobState',
204
+ 'browserTabState',
205
+ 'handoffVisibility',
206
+ 'reviewReadyClaimed',
207
+ 'browserBindingHash',
208
+ 'handoffEvidenceFingerprint',
209
+ 'handoffEvidenceType',
210
+ 'checkpointStatus',
211
+ 'checkpointMemberVersion',
212
+ 'checkpointInspectionEpoch',
213
+ 'checkpointLifecycle',
214
+ 'checkpointAction',
215
+ 'checkpointActionCount',
89
216
  ]),
90
217
  reconciliation: new Set([
91
218
  'workMode',
@@ -126,7 +253,108 @@ function requireCount(errors, receipt, field) {
126
253
  }
127
254
  }
128
255
 
129
- function validateCurrentLineage(errors, receipt, expectedContext, jobSetField = 'approvedJobIds') {
256
+ function requireFingerprint(errors, value, field) {
257
+ if (typeof value !== 'string' || !SHA256_PATTERN.test(value)) {
258
+ errors.push(`${field} must be a lowercase SHA-256 fingerprint`);
259
+ }
260
+ }
261
+
262
+ function requirePositiveDecimalId(errors, value, field) {
263
+ if (typeof value !== 'string' || !/^[1-9][0-9]*$/.test(value)) {
264
+ errors.push(`${field} must be a positive decimal identifier`);
265
+ }
266
+ }
267
+
268
+ function requireFutureIsoDateTime(errors, value, field, validationTimeMs) {
269
+ const canonicalPattern = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$/;
270
+ const parsedTime = typeof value === 'string' && canonicalPattern.test(value)
271
+ ? Date.parse(value)
272
+ : Number.NaN;
273
+ if (Number.isNaN(parsedTime) || new Date(parsedTime).toISOString() !== value) {
274
+ errors.push(`${field} must be an ISO-8601 date-time`);
275
+ } else if (parsedTime <= validationTimeMs) {
276
+ errors.push(`${field} must be later than validation time`);
277
+ }
278
+ }
279
+
280
+ function canonicalJson(value) {
281
+ if (value === null || typeof value === 'boolean' || typeof value === 'number') {
282
+ return JSON.stringify(value);
283
+ }
284
+ if (typeof value === 'string') return JSON.stringify(value);
285
+ if (Array.isArray(value)) return `[${value.map(canonicalJson).join(',')}]`;
286
+ return `{${Object.entries(value)
287
+ .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0))
288
+ .map(([key, item]) => `${JSON.stringify(key)}:${canonicalJson(item)}`)
289
+ .join(',')}}`;
290
+ }
291
+
292
+ function validateTruthCertificationMemberRuns(errors, memberRuns, receipt, side) {
293
+ const push = (message) => errors.push(`${side}: ${message}`);
294
+ if (!Array.isArray(memberRuns) || memberRuns.length < 1 || memberRuns.length > 100) {
295
+ push('truthCertificationMemberRuns must contain 1 to 100 member bindings');
296
+ return;
297
+ }
298
+ const allowedFields = ['memberId', 'runId', 'memberVersion', 'inspectionEpoch'];
299
+ const normalized = [];
300
+ for (const member of memberRuns) {
301
+ if (!member || typeof member !== 'object' || Array.isArray(member)
302
+ || Object.keys(member).length !== allowedFields.length
303
+ || Object.keys(member).some((field) => !allowedFields.includes(field))) {
304
+ push('truthCertificationMemberRuns entries must contain only memberId, runId, memberVersion, and inspectionEpoch');
305
+ return;
306
+ }
307
+ if (!Number.isSafeInteger(member.memberId) || member.memberId < 1
308
+ || !Number.isSafeInteger(member.runId) || member.runId < 1
309
+ || !Number.isSafeInteger(member.memberVersion) || member.memberVersion < 1
310
+ || !Number.isSafeInteger(member.inspectionEpoch) || member.inspectionEpoch < 0) {
311
+ push('truthCertificationMemberRuns entries contain an invalid identifier, version, or epoch');
312
+ return;
313
+ }
314
+ normalized.push({
315
+ memberId: member.memberId,
316
+ runId: member.runId,
317
+ memberVersion: member.memberVersion,
318
+ inspectionEpoch: member.inspectionEpoch,
319
+ });
320
+ }
321
+ normalized.sort((left, right) => left.memberId - right.memberId || left.runId - right.runId);
322
+ if (new Set(normalized.map((member) => member.memberId)).size !== normalized.length) {
323
+ push('truthCertificationMemberRuns memberId values must be unique');
324
+ }
325
+ const runSetHash = createHash('sha256').update(canonicalJson(normalized)).digest('hex');
326
+ if (runSetHash !== receipt.truthCertificationRunSetHash) {
327
+ push('truthCertificationMemberRuns must match truthCertificationRunSetHash');
328
+ }
329
+ const currentMemberMatches = normalized.filter((member) => (
330
+ member.memberId === receipt.memberId
331
+ && member.runId === receipt.runId
332
+ && member.memberVersion === receipt.checkpointMemberVersion
333
+ && member.inspectionEpoch === receipt.inspectionEpoch
334
+ ));
335
+ if (currentMemberMatches.length !== 1) {
336
+ push('truth certification must cover the current member, run, checkpoint version, and inspection epoch');
337
+ }
338
+ }
339
+
340
+ function requireExactObject(errors, value, field, allowedFields) {
341
+ if (!value || typeof value !== 'object' || Array.isArray(value)) {
342
+ errors.push(`${field} must be an object`);
343
+ return false;
344
+ }
345
+ for (const key of Object.keys(value)) {
346
+ if (!allowedFields.includes(key)) errors.push(`${field} contains an unexpected field`);
347
+ }
348
+ return true;
349
+ }
350
+
351
+ function validateCurrentLineage(
352
+ errors,
353
+ receipt,
354
+ expectedContext,
355
+ jobSetField = 'approvedJobIds',
356
+ additionalExpectedFields = [],
357
+ ) {
130
358
  if (!WORK_MODES.has(receipt.workMode)) errors.push('workMode must be accessible_execution or fixed_inspection');
131
359
  for (const field of ['batchId', 'memberId', 'jobId', 'runId']) requireTracklyId(errors, receipt, field);
132
360
  if (!Number.isSafeInteger(receipt.inspectionEpoch) || receipt.inspectionEpoch < 0) {
@@ -141,10 +369,10 @@ function validateCurrentLineage(errors, receipt, expectedContext, jobSetField =
141
369
  errors.push('expectedContext is required');
142
370
  return;
143
371
  }
144
- const expectedFields = new Set([...COMMON_LINEAGE_FIELDS, jobSetField]);
372
+ const expectedFields = new Set([...COMMON_LINEAGE_FIELDS, jobSetField, ...additionalExpectedFields]);
145
373
  if (receipt.workMode === 'accessible_execution') expectedFields.add('executionId');
146
374
  for (const field of Object.keys(expectedContext)) {
147
- if (!expectedFields.has(field)) errors.push(`unexpected expectedContext field: ${field}`);
375
+ if (!expectedFields.has(field)) errors.push('expectedContext contains an unexpected field');
148
376
  }
149
377
  for (const field of COMMON_LINEAGE_FIELDS) {
150
378
  if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
@@ -179,7 +407,7 @@ function validateSelection(receipt, expectedContext) {
179
407
  if (!Number.isInteger(receipt.latestExplicitTarget)
180
408
  || receipt.latestExplicitTarget < 1
181
409
  || receipt.latestExplicitTarget > maximumTarget) {
182
- errors.push(`latestExplicitTarget must be an integer from 1 to ${maximumTarget} for ${receipt.workMode || 'the selected work mode'}`);
410
+ errors.push(`latestExplicitTarget must be an integer from 1 to ${maximumTarget} for the selected work mode`);
183
411
  }
184
412
  if (!Array.isArray(receipt.approvedJobIds)) {
185
413
  errors.push('approvedJobIds must be an array');
@@ -205,7 +433,7 @@ function validateSelection(receipt, expectedContext) {
205
433
  const expectedFields = new Set(['workMode', 'batchId', 'latestExplicitTarget', 'selectableJobIds', 'queueExhausted']);
206
434
  if (receipt.workMode === 'accessible_execution') expectedFields.add('executionId');
207
435
  for (const field of Object.keys(expectedContext)) {
208
- if (!expectedFields.has(field)) errors.push(`unexpected expectedContext field: ${field}`);
436
+ if (!expectedFields.has(field)) errors.push('expectedContext contains an unexpected field');
209
437
  }
210
438
  if (receipt.workMode !== expectedContext.workMode) errors.push('workMode must match expectedContext');
211
439
  if (receipt.batchId !== expectedContext.batchId) errors.push('batchId must match expectedContext');
@@ -251,20 +479,131 @@ function validateAccess(receipt, expectedContext) {
251
479
 
252
480
  function validateFill(receipt, expectedContext) {
253
481
  const errors = [];
254
- validateCurrentLineage(errors, receipt, expectedContext);
482
+ validateCurrentLineage(errors, receipt, expectedContext, 'approvedJobIds', [
483
+ 'profileRevision',
484
+ 'canonicalEducationRecordCount',
485
+ 'canonicalEmploymentPositionCount',
486
+ 'formInventoryFingerprint',
487
+ 'resumeControl',
488
+ ]);
489
+ if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
490
+ if (receipt.workMode === 'accessible_execution') {
491
+ requireTracklyId(errors, receipt, 'profileRevision');
492
+ requireTracklyId(errors, expectedContext, 'profileRevision');
493
+ } else {
494
+ requireCount(errors, receipt, 'profileRevision');
495
+ requireCount(errors, expectedContext, 'profileRevision');
496
+ }
497
+ if (receipt.profileRevision !== expectedContext.profileRevision) {
498
+ errors.push('profileRevision must match expectedContext');
499
+ }
500
+ requireCount(errors, expectedContext, 'canonicalEducationRecordCount');
501
+ requireCount(errors, expectedContext, 'canonicalEmploymentPositionCount');
502
+ requireFingerprint(errors, expectedContext.formInventoryFingerprint, 'expectedContext.formInventoryFingerprint');
503
+ if (!['required', 'optional', 'absent'].includes(expectedContext.resumeControl)) {
504
+ errors.push('expectedContext.resumeControl must be required, optional, or absent');
505
+ }
506
+ }
255
507
  for (const field of ['visibleControlCount', 'committedControlCount', 'typedExceptionCount', 'knownOmissionCount']) {
256
508
  requireCount(errors, receipt, field);
257
509
  }
510
+ if (receipt.visibleControlCount === 0) errors.push('visibleControlCount must be at least 1 for a fill checkpoint');
258
511
  if (Number.isSafeInteger(receipt.visibleControlCount)
259
512
  && Number.isSafeInteger(receipt.committedControlCount)
260
513
  && Number.isSafeInteger(receipt.typedExceptionCount)
261
514
  && receipt.committedControlCount + receipt.typedExceptionCount !== receipt.visibleControlCount) {
262
515
  errors.push('committedControlCount plus typedExceptionCount must equal visibleControlCount');
263
516
  }
517
+ if (requireExactObject(errors, receipt.controlAccounting, 'controlAccounting', CONTROL_ACCOUNTING_FIELDS)) {
518
+ for (const field of CONTROL_ACCOUNTING_FIELDS) requireCount(errors, receipt.controlAccounting, field);
519
+ const accountingCount = CONTROL_ACCOUNTING_FIELDS
520
+ .reduce((total, field) => total + (Number.isSafeInteger(receipt.controlAccounting[field]) ? receipt.controlAccounting[field] : 0), 0);
521
+ const committedCount = COMMITTED_CONTROL_FIELDS
522
+ .reduce((total, field) => total + (Number.isSafeInteger(receipt.controlAccounting[field]) ? receipt.controlAccounting[field] : 0), 0);
523
+ const exceptionCount = EXCEPTION_CONTROL_FIELDS
524
+ .reduce((total, field) => total + (Number.isSafeInteger(receipt.controlAccounting[field]) ? receipt.controlAccounting[field] : 0), 0);
525
+ if (Number.isSafeInteger(receipt.visibleControlCount) && accountingCount !== receipt.visibleControlCount) {
526
+ errors.push('controlAccounting counts must equal visibleControlCount');
527
+ }
528
+ if (Number.isSafeInteger(receipt.committedControlCount) && committedCount !== receipt.committedControlCount) {
529
+ errors.push('committed control accounting must equal committedControlCount');
530
+ }
531
+ if (Number.isSafeInteger(receipt.typedExceptionCount) && exceptionCount !== receipt.typedExceptionCount) {
532
+ errors.push('typed exception accounting must equal typedExceptionCount');
533
+ }
534
+ }
535
+ requireFingerprint(errors, receipt.formInventoryFingerprint, 'formInventoryFingerprint');
536
+ if (receipt.formInventoryFingerprint !== expectedContext?.formInventoryFingerprint) {
537
+ errors.push('formInventoryFingerprint must match expectedContext');
538
+ }
264
539
  if (receipt.knownOmissionCount !== 0) errors.push('knownOmissionCount must be 0');
265
540
  requireTrue(errors, receipt, 'knownFieldsFilledBeforeQuestions');
541
+ requireTrue(errors, receipt, 'answerLookupCompleted');
542
+ if (requireExactObject(errors, receipt.answerLookupScopeCounts, 'answerLookupScopeCounts', ANSWER_SCOPE_FIELDS)) {
543
+ for (const field of ANSWER_SCOPE_FIELDS) requireCount(errors, receipt.answerLookupScopeCounts, field);
544
+ }
545
+ requireFingerprint(errors, receipt.answerLookupFingerprint, 'answerLookupFingerprint');
266
546
  requireTrue(errors, receipt, 'parserSensitiveFieldsRechecked');
267
547
  requireTrue(errors, receipt, 'educationAndEmploymentVerified');
548
+ if (requireExactObject(errors, receipt.historyReconciliation, 'historyReconciliation', HISTORY_FIELDS)) {
549
+ for (const field of [
550
+ 'canonicalEducationRecordCount',
551
+ 'accountedEducationRecordCount',
552
+ 'canonicalEmploymentPositionCount',
553
+ 'accountedEmploymentPositionCount',
554
+ ]) requireCount(errors, receipt.historyReconciliation, field);
555
+ if (receipt.historyReconciliation.canonicalEducationRecordCount
556
+ !== receipt.historyReconciliation.accountedEducationRecordCount) {
557
+ errors.push('education record counts must match');
558
+ }
559
+ if (receipt.historyReconciliation.canonicalEmploymentPositionCount
560
+ !== receipt.historyReconciliation.accountedEmploymentPositionCount) {
561
+ errors.push('employment position counts must match');
562
+ }
563
+ if (receipt.historyReconciliation.canonicalEducationRecordCount
564
+ !== expectedContext?.canonicalEducationRecordCount) {
565
+ errors.push('canonical education record count must match expectedContext');
566
+ }
567
+ if (receipt.historyReconciliation.canonicalEmploymentPositionCount
568
+ !== expectedContext?.canonicalEmploymentPositionCount) {
569
+ errors.push('canonical employment position count must match expectedContext');
570
+ }
571
+ requireTrue(errors, receipt.historyReconciliation, 'educationOrderVerified');
572
+ requireTrue(errors, receipt.historyReconciliation, 'employmentOrderVerified');
573
+ requireFalse(errors, receipt.historyReconciliation, 'datePrecisionInvented');
574
+ requireFingerprint(errors, receipt.historyReconciliation.educationReconciliationFingerprint, 'historyReconciliation.educationReconciliationFingerprint');
575
+ requireFingerprint(errors, receipt.historyReconciliation.employmentReconciliationFingerprint, 'historyReconciliation.employmentReconciliationFingerprint');
576
+ }
577
+ if (requireExactObject(errors, receipt.resumeAudit, 'resumeAudit', RESUME_AUDIT_FIELDS)) {
578
+ if (!['required', 'optional', 'absent'].includes(receipt.resumeAudit.control)) {
579
+ errors.push('resumeAudit.control must be required, optional, or absent');
580
+ }
581
+ if (receipt.resumeAudit.control !== expectedContext?.resumeControl) {
582
+ errors.push('resumeAudit.control must match expectedContext');
583
+ }
584
+ const attachmentExpected = ['required', 'optional'].includes(receipt.resumeAudit.control);
585
+ const allowedModes = attachmentExpected
586
+ ? ['automated_verified', 'manual_unbound']
587
+ : ['not_applicable'];
588
+ if (!allowedModes.includes(receipt.resumeAudit.mode)) {
589
+ errors.push('resumeAudit.mode must match the declared control mode');
590
+ }
591
+ const expectedOutcomes = attachmentExpected
592
+ ? {
593
+ approval: 'passed',
594
+ preAttachVerification: receipt.resumeAudit.mode === 'manual_unbound' ? 'not_applicable' : 'passed',
595
+ attachmentCommit: receipt.resumeAudit.mode === 'manual_unbound' ? 'user_confirmed' : 'passed',
596
+ filenameVerification: 'passed',
597
+ parserRecheck: 'passed',
598
+ finalSweep: 'passed',
599
+ }
600
+ : Object.fromEntries(RESUME_AUDIT_FIELDS.slice(2).map((field) => [field, 'not_applicable']));
601
+ for (const [field, expected] of Object.entries(expectedOutcomes)) {
602
+ if (receipt.resumeAudit[field] !== expected) {
603
+ errors.push(`resumeAudit.${field} must match the declared control mode`);
604
+ }
605
+ }
606
+ }
268
607
  if (typeof receipt.writingPresent !== 'boolean') errors.push('writingPresent must be a boolean');
269
608
  if (!['passed', 'not_applicable'].includes(receipt.localWritingGate)) {
270
609
  errors.push('localWritingGate must be passed or not_applicable');
@@ -295,23 +634,330 @@ function validateFill(receipt, expectedContext) {
295
634
  return errors;
296
635
  }
297
636
 
298
- function validateReview(receipt, expectedContext) {
637
+ function validateReview(receipt, expectedContext, priorFill, validationTimeMs) {
299
638
  const errors = [];
300
- validateCurrentLineage(errors, receipt, expectedContext);
639
+ const fillBaselineFields = [
640
+ 'profileRevision',
641
+ 'formInventoryFingerprint',
642
+ ];
643
+ const resolutionFields = [
644
+ 'resolvedActionCount',
645
+ 'resolvedActionIdsFingerprint',
646
+ ];
647
+ const checkpointFields = [
648
+ 'checkpointAction',
649
+ 'continuationAllowed',
650
+ 'checkpointStatus',
651
+ 'checkpointMemberVersion',
652
+ 'checkpointInspectionEpoch',
653
+ 'checkpointLifecycle',
654
+ 'checkpointActionCount',
655
+ 'checkpointActionIdsFingerprint',
656
+ ];
657
+ const truthCertificationFields = [
658
+ 'truthCertificationAttestationId',
659
+ 'truthCertificationDependencyHash',
660
+ 'truthCertificationExpiresAt',
661
+ 'truthCertificationMemberRuns',
662
+ 'truthCertificationRunSetHash',
663
+ 'truthCertificationStatus',
664
+ ];
665
+ const visibilityFields = [
666
+ 'browserBindingHash',
667
+ 'handoffEvidenceFingerprint',
668
+ 'handoffEvidenceType',
669
+ ];
670
+ validateCurrentLineage(
671
+ errors,
672
+ receipt,
673
+ expectedContext,
674
+ 'approvedJobIds',
675
+ [
676
+ ...fillBaselineFields,
677
+ ...resolutionFields,
678
+ ...checkpointFields,
679
+ ...truthCertificationFields,
680
+ ...visibilityFields,
681
+ ],
682
+ );
683
+ if (receipt.workMode === 'accessible_execution') {
684
+ requireTracklyId(errors, receipt, 'profileRevision');
685
+ } else {
686
+ requireCount(errors, receipt, 'profileRevision');
687
+ }
688
+ requireFingerprint(errors, receipt.formInventoryFingerprint, 'formInventoryFingerprint');
689
+ if (!priorFill || typeof priorFill !== 'object' || Array.isArray(priorFill)) {
690
+ errors.push('priorFill is required for review');
691
+ } else {
692
+ const priorFillFields = Object.keys(priorFill);
693
+ if (priorFillFields.length !== 2
694
+ || !priorFillFields.includes('receipt')
695
+ || !priorFillFields.includes('expectedContext')) {
696
+ errors.push('priorFill must contain only receipt and expectedContext');
697
+ } else {
698
+ const priorFillErrors = validateCheckpoint(
699
+ 'fill',
700
+ priorFill.receipt,
701
+ priorFill.expectedContext,
702
+ );
703
+ errors.push(...priorFillErrors.map((error) => `priorFill: ${error}`));
704
+ if (priorFill.receipt && typeof priorFill.receipt === 'object' && !Array.isArray(priorFill.receipt)) {
705
+ for (const field of [
706
+ 'missingFact',
707
+ 'liveConsent',
708
+ 'authenticationBlocker',
709
+ 'unobservableCommit',
710
+ 'unsupportedControl',
711
+ ]) {
712
+ if (priorFill.receipt.controlAccounting?.[field] !== 0) {
713
+ errors.push(`priorFill.controlAccounting.${field} must be 0 before review`);
714
+ }
715
+ }
716
+ const lineageFields = [...COMMON_LINEAGE_FIELDS];
717
+ if (receipt.workMode === 'accessible_execution') lineageFields.push('executionId');
718
+ for (const field of [...lineageFields, ...fillBaselineFields]) {
719
+ if (receipt[field] !== priorFill.receipt[field]) {
720
+ errors.push(`${field} must match the validated priorFill receipt`);
721
+ }
722
+ }
723
+ }
724
+ }
725
+ }
301
726
  requireTrue(errors, receipt, 'finalIntegrityPassed');
302
727
  requireTrue(errors, receipt, 'truthConfirmationRecorded');
728
+ requirePositiveDecimalId(
729
+ errors,
730
+ receipt.truthCertificationAttestationId,
731
+ 'truthCertificationAttestationId',
732
+ );
733
+ requireFingerprint(
734
+ errors,
735
+ receipt.truthCertificationDependencyHash,
736
+ 'truthCertificationDependencyHash',
737
+ );
738
+ requireFutureIsoDateTime(
739
+ errors,
740
+ receipt.truthCertificationExpiresAt,
741
+ 'truthCertificationExpiresAt',
742
+ validationTimeMs,
743
+ );
744
+ requireFingerprint(
745
+ errors,
746
+ receipt.truthCertificationRunSetHash,
747
+ 'truthCertificationRunSetHash',
748
+ );
749
+ validateTruthCertificationMemberRuns(errors, receipt.truthCertificationMemberRuns, receipt, 'receipt');
750
+ validateTruthCertificationMemberRuns(
751
+ errors,
752
+ expectedContext?.truthCertificationMemberRuns,
753
+ expectedContext || {},
754
+ 'expectedContext',
755
+ );
756
+ if (Array.isArray(receipt.truthCertificationMemberRuns)
757
+ && Array.isArray(expectedContext?.truthCertificationMemberRuns)
758
+ && canonicalJson(receipt.truthCertificationMemberRuns)
759
+ !== canonicalJson(expectedContext.truthCertificationMemberRuns)) {
760
+ errors.push('truthCertificationMemberRuns must match the authoritative expectedContext');
761
+ }
762
+ if (!['recorded', 'replayed'].includes(receipt.truthCertificationStatus)) {
763
+ errors.push('truthCertificationStatus must be recorded or replayed');
764
+ }
303
765
  requireFalse(errors, receipt, 'submitActivated');
304
766
  requireTrue(errors, receipt, 'reviewTabPreserved');
305
767
  requireTrue(errors, receipt, 'userVisibleHandoffProven');
768
+ requireFingerprint(errors, receipt.browserBindingHash, 'browserBindingHash');
769
+ requireFingerprint(errors, receipt.handoffEvidenceFingerprint, 'handoffEvidenceFingerprint');
770
+ if (!['visible_presentation_receipt', 'user_visible_handoff_receipt'].includes(receipt.handoffEvidenceType)) {
771
+ errors.push('handoffEvidenceType must identify visible presentation or an exact tab-bound user-visible handoff receipt');
772
+ }
773
+ if (receipt.checkpointAction !== 'review/manual_submit') {
774
+ errors.push('checkpointAction must be review/manual_submit');
775
+ }
776
+ if (receipt.continuationAllowed !== false) {
777
+ errors.push('continuationAllowed must be false for review/manual_submit');
778
+ }
779
+ requireCount(errors, receipt, 'resolvedActionCount');
780
+ requireFingerprint(errors, receipt.resolvedActionIdsFingerprint, 'resolvedActionIdsFingerprint');
781
+ if (!['recorded', 'replayed'].includes(receipt.checkpointStatus)) {
782
+ errors.push('checkpointStatus must be recorded or replayed');
783
+ }
784
+ requireTracklyId(errors, receipt, 'checkpointMemberVersion');
785
+ if (!Number.isSafeInteger(receipt.checkpointInspectionEpoch) || receipt.checkpointInspectionEpoch < 0) {
786
+ errors.push('checkpointInspectionEpoch must be a non-negative safe integer');
787
+ }
788
+ if (receipt.checkpointInspectionEpoch !== receipt.inspectionEpoch) {
789
+ errors.push('checkpointInspectionEpoch must equal inspectionEpoch');
790
+ }
791
+ if (receipt.checkpointLifecycle !== 'review_ready') {
792
+ errors.push('checkpointLifecycle must be review_ready');
793
+ }
794
+ requireCount(errors, receipt, 'checkpointActionCount');
795
+ if (receipt.checkpointActionCount !== 1) {
796
+ errors.push('review/manual_submit checkpoint must contain exactly one action');
797
+ }
798
+ requireFingerprint(errors, receipt.checkpointActionIdsFingerprint, 'checkpointActionIdsFingerprint');
799
+ if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
800
+ for (const field of [
801
+ ...fillBaselineFields,
802
+ ...resolutionFields,
803
+ ...checkpointFields,
804
+ ...truthCertificationFields.filter((field) => field !== 'truthCertificationMemberRuns'),
805
+ ...visibilityFields,
806
+ ]) {
807
+ if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
808
+ }
809
+ }
810
+ return errors;
811
+ }
812
+
813
+ function validateHandoff(receipt, expectedContext) {
814
+ const errors = [];
815
+ const evidenceFields = ['browserBindingHash', 'handoffEvidenceFingerprint', 'handoffEvidenceType'];
816
+ const stateAuthorityFields = [
817
+ 'employerApplicationState',
818
+ 'tracklyMemberState',
819
+ 'tracklyJobState',
820
+ ];
821
+ const checkpointAuthorityFields = [
822
+ 'checkpointStatus',
823
+ 'checkpointMemberVersion',
824
+ 'checkpointInspectionEpoch',
825
+ 'checkpointLifecycle',
826
+ 'checkpointAction',
827
+ 'checkpointActionCount',
828
+ ];
829
+ const reviewAuthorityFields = [...stateAuthorityFields, ...checkpointAuthorityFields];
830
+ validateCurrentLineage(
831
+ errors,
832
+ receipt,
833
+ expectedContext,
834
+ 'approvedJobIds',
835
+ [...evidenceFields, ...reviewAuthorityFields],
836
+ );
837
+ if (!EMPLOYER_APPLICATION_STATES.has(receipt.employerApplicationState)) {
838
+ errors.push('employerApplicationState is invalid');
839
+ }
840
+ if (!TRACKLY_MEMBER_STATES.has(receipt.tracklyMemberState)) {
841
+ errors.push('tracklyMemberState is invalid');
842
+ }
843
+ if (!TRACKLY_JOB_STATES.has(receipt.tracklyJobState)) {
844
+ errors.push('tracklyJobState is invalid');
845
+ }
846
+ if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
847
+ for (const field of stateAuthorityFields) {
848
+ if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
849
+ }
850
+ }
851
+ if (receipt.browserTabState === 'closed_verified') {
852
+ errors.push('closed_verified is reserved for reconciliation receipts');
853
+ } else if (!BROWSER_HANDOFF_STATES.has(receipt.browserTabState)) {
854
+ errors.push('browserTabState is invalid');
855
+ }
856
+ if (!['verified', 'unverified'].includes(receipt.handoffVisibility)) {
857
+ errors.push('handoffVisibility must be verified or unverified');
858
+ }
859
+ if (typeof receipt.reviewReadyClaimed !== 'boolean') {
860
+ errors.push('reviewReadyClaimed must be a boolean');
861
+ }
862
+ if (receipt.handoffVisibility === 'unverified' && receipt.reviewReadyClaimed !== false) {
863
+ errors.push('reviewReadyClaimed must be false when handoff visibility is unverified');
864
+ }
865
+ if (receipt.browserTabState === 'durable_handoff_proven'
866
+ && receipt.handoffVisibility !== 'verified') {
867
+ errors.push('durable_handoff_proven requires verified visibility');
868
+ }
869
+ if (receipt.handoffVisibility === 'verified'
870
+ && !['visible', 'durable_handoff_proven'].includes(receipt.browserTabState)) {
871
+ errors.push('verified handoff requires a visible or durably handed-off browser tab');
872
+ }
873
+ if (receipt.handoffVisibility === 'verified') {
874
+ requireFingerprint(errors, receipt.browserBindingHash, 'browserBindingHash');
875
+ requireFingerprint(errors, receipt.handoffEvidenceFingerprint, 'handoffEvidenceFingerprint');
876
+ if (![
877
+ 'visible_presentation_receipt',
878
+ 'user_visible_handoff_receipt',
879
+ 'durable_handoff_receipt',
880
+ ].includes(receipt.handoffEvidenceType)) {
881
+ errors.push('handoffEvidenceType must identify visible presentation, a user-visible handoff receipt, or a durable handoff receipt');
882
+ }
883
+ if (receipt.browserTabState === 'visible'
884
+ && !['visible_presentation_receipt', 'user_visible_handoff_receipt'].includes(receipt.handoffEvidenceType)) {
885
+ errors.push('visible browser state requires presentation or exact tab-bound user-visible handoff evidence');
886
+ }
887
+ if (receipt.browserTabState === 'durable_handoff_proven'
888
+ && receipt.handoffEvidenceType !== 'durable_handoff_receipt') {
889
+ errors.push('durable_handoff_proven requires durable_handoff_receipt evidence');
890
+ }
891
+ if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
892
+ for (const field of evidenceFields) {
893
+ if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
894
+ }
895
+ }
896
+ } else if (evidenceFields.some((field) => Object.hasOwn(receipt, field))) {
897
+ errors.push('handoff evidence fields must be omitted when visibility is unverified');
898
+ }
899
+ if (receipt.reviewReadyClaimed === true
900
+ && (receipt.employerApplicationState !== 'review_state_prepared'
901
+ || receipt.tracklyMemberState !== 'awaiting_manual_submit'
902
+ || receipt.tracklyJobState !== 'check_later'
903
+ || receipt.handoffVisibility !== 'verified')) {
904
+ errors.push('reviewReadyClaimed requires prepared employer state, awaiting_manual_submit member state, check_later job state, and verified visibility');
905
+ }
906
+ if (receipt.reviewReadyClaimed === true) {
907
+ if (!['recorded', 'replayed'].includes(receipt.checkpointStatus)) {
908
+ errors.push('reviewReadyClaimed requires a recorded or replayed checkpointStatus');
909
+ }
910
+ requireTracklyId(errors, receipt, 'checkpointMemberVersion');
911
+ if (!Number.isSafeInteger(receipt.checkpointInspectionEpoch) || receipt.checkpointInspectionEpoch < 0) {
912
+ errors.push('checkpointInspectionEpoch must be a non-negative safe integer');
913
+ }
914
+ if (receipt.checkpointInspectionEpoch !== receipt.inspectionEpoch) {
915
+ errors.push('checkpointInspectionEpoch must equal inspectionEpoch');
916
+ }
917
+ if (receipt.checkpointLifecycle !== 'review_ready') {
918
+ errors.push('reviewReadyClaimed requires checkpointLifecycle review_ready');
919
+ }
920
+ if (receipt.checkpointAction !== 'review/manual_submit') {
921
+ errors.push('reviewReadyClaimed requires checkpointAction review/manual_submit');
922
+ }
923
+ if (receipt.checkpointActionCount !== 1) {
924
+ errors.push('reviewReadyClaimed requires checkpointActionCount 1');
925
+ }
926
+ if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
927
+ for (const field of checkpointAuthorityFields) {
928
+ if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
929
+ }
930
+ }
931
+ } else {
932
+ if (checkpointAuthorityFields.some((field) => Object.hasOwn(receipt, field))) {
933
+ errors.push('checkpoint authority fields must be omitted when reviewReadyClaimed is false');
934
+ }
935
+ }
306
936
  return errors;
307
937
  }
308
938
 
309
939
  function validateReconciliation(receipt, expectedContext) {
310
940
  const errors = [];
311
- validateCurrentLineage(errors, receipt, expectedContext);
941
+ const reconciliationAuthorityFields = [
942
+ 'positiveSubmissionEvidenceRecorded',
943
+ 'memberLifecycle',
944
+ 'tracklyJobStatus',
945
+ ];
946
+ validateCurrentLineage(
947
+ errors,
948
+ receipt,
949
+ expectedContext,
950
+ 'approvedJobIds',
951
+ reconciliationAuthorityFields,
952
+ );
312
953
  requireTrue(errors, receipt, 'positiveSubmissionEvidenceRecorded');
313
954
  if (receipt.memberLifecycle !== 'submitted') errors.push('memberLifecycle must be submitted');
314
955
  if (receipt.tracklyJobStatus !== 'applied_confirmed') errors.push('tracklyJobStatus must be applied_confirmed');
956
+ if (expectedContext && typeof expectedContext === 'object' && !Array.isArray(expectedContext)) {
957
+ for (const field of reconciliationAuthorityFields) {
958
+ if (receipt[field] !== expectedContext[field]) errors.push(`${field} must match expectedContext`);
959
+ }
960
+ }
315
961
  if (!CLEANUP_PREFERENCES.has(receipt.cleanupPreference)) errors.push('cleanupPreference is invalid');
316
962
  if (!TAB_STATES.has(receipt.browserTabStatus)) errors.push('browserTabStatus is invalid');
317
963
  if (receipt.cleanupPreference === 'never' && receipt.browserTabStatus === 'closed_verified') {
@@ -336,36 +982,37 @@ const VALIDATORS = {
336
982
  access: validateAccess,
337
983
  fill: validateFill,
338
984
  review: validateReview,
985
+ handoff: validateHandoff,
339
986
  reconciliation: validateReconciliation,
340
987
  };
341
988
 
342
- function validateCheckpoint(phase, receipt, expectedContext) {
989
+ function validateCheckpoint(phase, receipt, expectedContext, priorFill, validationTimeMs = Date.now()) {
343
990
  if (!Object.hasOwn(VALIDATORS, phase)) return [`unknown phase: ${phase}`];
344
991
  if (!receipt || typeof receipt !== 'object' || Array.isArray(receipt)) return ['receipt must be a JSON object'];
992
+ if (phase !== 'review' && priorFill !== undefined) return ['priorFill is permitted only for review'];
345
993
  const unexpectedFields = Object.keys(receipt)
346
994
  .filter((field) => !PHASE_FIELDS[phase].has(field))
347
- .map((field) => `unexpected field: ${field}`);
348
- return [...unexpectedFields, ...VALIDATORS[phase](receipt, expectedContext)];
995
+ .map(() => 'receipt contains an unexpected field');
996
+ return [
997
+ ...unexpectedFields,
998
+ ...VALIDATORS[phase](receipt, expectedContext, priorFill, validationTimeMs),
999
+ ];
349
1000
  }
350
1001
 
351
1002
  async function readReceiptInput(stream) {
352
1003
  const decoder = new StringDecoder('utf8');
353
1004
  let input = '';
354
1005
  let inputBytes = 0;
355
- let oversized = false;
356
1006
  for await (const chunk of stream) {
357
- if (oversized) continue;
358
1007
  const bytes = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
359
1008
  inputBytes += bytes.length;
360
1009
  if (inputBytes > MAX_RECEIPT_BYTES) {
361
- oversized = true;
362
- input = '';
363
- continue;
1010
+ return { input: '', oversized: true };
364
1011
  }
365
1012
  input += decoder.write(bytes);
366
1013
  }
367
- if (!oversized) input += decoder.end();
368
- return { input, oversized };
1014
+ input += decoder.end();
1015
+ return { input, oversized: false };
369
1016
  }
370
1017
 
371
1018
  async function main() {
@@ -391,18 +1038,18 @@ async function main() {
391
1038
  }
392
1039
  if (!envelope || typeof envelope !== 'object' || Array.isArray(envelope)
393
1040
  || !Object.hasOwn(envelope, 'receipt')) {
394
- process.stderr.write('input must be an envelope with receipt and optional expectedContext\n');
1041
+ process.stderr.write('input must be an envelope with receipt, expectedContext, and priorFill for review\n');
395
1042
  process.exitCode = 1;
396
1043
  return;
397
1044
  }
398
1045
  const unexpectedEnvelopeFields = Object.keys(envelope)
399
- .filter((field) => !['receipt', 'expectedContext'].includes(field));
1046
+ .filter((field) => !['receipt', 'expectedContext', 'priorFill'].includes(field));
400
1047
  if (unexpectedEnvelopeFields.length > 0) {
401
- process.stderr.write(`${unexpectedEnvelopeFields.map((field) => `unexpected envelope field: ${field}`).join('\n')}\n`);
1048
+ process.stderr.write('input envelope contains unsupported fields\n');
402
1049
  process.exitCode = 1;
403
1050
  return;
404
1051
  }
405
- const errors = validateCheckpoint(phase, envelope.receipt, envelope.expectedContext);
1052
+ const errors = validateCheckpoint(phase, envelope.receipt, envelope.expectedContext, envelope.priorFill);
406
1053
  if (errors.length > 0) {
407
1054
  process.stderr.write(`${errors.join('\n')}\n`);
408
1055
  process.exitCode = 1;