reviewflow 3.36.1 → 3.37.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.
- package/CHANGELOG.md +16 -0
- package/dist/main/server.d.ts +18 -0
- package/dist/main/server.d.ts.map +1 -1
- package/dist/main/server.js +24 -5
- package/dist/main/server.js.map +1 -1
- package/dist/modules/platform-integration/interface-adapters/controllers/webhook/gitlab.controller.d.ts.map +1 -1
- package/dist/modules/platform-integration/interface-adapters/controllers/webhook/gitlab.controller.js +12 -2
- package/dist/modules/platform-integration/interface-adapters/controllers/webhook/gitlab.controller.js.map +1 -1
- package/dist/modules/platform-integration/services/pinnedThreadFetchTarget.d.ts +22 -1
- package/dist/modules/platform-integration/services/pinnedThreadFetchTarget.d.ts.map +1 -1
- package/dist/modules/platform-integration/services/pinnedThreadFetchTarget.js +21 -1
- package/dist/modules/platform-integration/services/pinnedThreadFetchTarget.js.map +1 -1
- package/dist/tests/acceptance/196-least-privilege-platform-token.acceptance.test.d.ts +2 -0
- package/dist/tests/acceptance/196-least-privilege-platform-token.acceptance.test.d.ts.map +1 -0
- package/dist/tests/acceptance/196-least-privilege-platform-token.acceptance.test.js +197 -0
- package/dist/tests/acceptance/196-least-privilege-platform-token.acceptance.test.js.map +1 -0
- package/dist/tests/acceptance/197-trusted-actor-provenance-gate.acceptance.test.d.ts +2 -0
- package/dist/tests/acceptance/197-trusted-actor-provenance-gate.acceptance.test.d.ts.map +1 -0
- package/dist/tests/acceptance/197-trusted-actor-provenance-gate.acceptance.test.js +413 -0
- package/dist/tests/acceptance/197-trusted-actor-provenance-gate.acceptance.test.js.map +1 -0
- package/dist/tests/acceptance/198-constrained-action-surface.acceptance.test.d.ts +2 -0
- package/dist/tests/acceptance/198-constrained-action-surface.acceptance.test.d.ts.map +1 -0
- package/dist/tests/acceptance/198-constrained-action-surface.acceptance.test.js +185 -0
- package/dist/tests/acceptance/198-constrained-action-surface.acceptance.test.js.map +1 -0
- package/dist/tests/acceptance/199-review-output-egress-scan.acceptance.test.d.ts +2 -0
- package/dist/tests/acceptance/199-review-output-egress-scan.acceptance.test.d.ts.map +1 -0
- package/dist/tests/acceptance/199-review-output-egress-scan.acceptance.test.js +159 -0
- package/dist/tests/acceptance/199-review-output-egress-scan.acceptance.test.js.map +1 -0
- package/dist/tests/acceptance/200-webhook-event-idempotency.acceptance.test.d.ts +2 -0
- package/dist/tests/acceptance/200-webhook-event-idempotency.acceptance.test.d.ts.map +1 -0
- package/dist/tests/acceptance/200-webhook-event-idempotency.acceptance.test.js +244 -0
- package/dist/tests/acceptance/200-webhook-event-idempotency.acceptance.test.js.map +1 -0
- package/dist/tests/acceptance/201-transport-provenance-hardening.acceptance.test.d.ts +2 -0
- package/dist/tests/acceptance/201-transport-provenance-hardening.acceptance.test.d.ts.map +1 -0
- package/dist/tests/acceptance/201-transport-provenance-hardening.acceptance.test.js +72 -0
- package/dist/tests/acceptance/201-transport-provenance-hardening.acceptance.test.js.map +1 -0
- package/dist/tests/units/interface-adapters/controllers/webhook/gitlab.controller.test.js +96 -0
- package/dist/tests/units/interface-adapters/controllers/webhook/gitlab.controller.test.js.map +1 -1
- package/dist/tests/units/modules/platform-integration/services/pinnedThreadFetchTarget.test.js +45 -2
- package/dist/tests/units/modules/platform-integration/services/pinnedThreadFetchTarget.test.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import { vi } from 'vitest';
|
|
2
|
+
const mockConfig = {
|
|
3
|
+
server: { port: 3000 },
|
|
4
|
+
user: { gitlabUsername: 'claude-bot', githubUsername: 'claude-bot' },
|
|
5
|
+
queue: { maxConcurrent: 1, deduplicationWindowMs: 60000 },
|
|
6
|
+
repositories: [],
|
|
7
|
+
};
|
|
8
|
+
const mockRepoConfig = {
|
|
9
|
+
name: 'test-project',
|
|
10
|
+
platform: 'gitlab',
|
|
11
|
+
localPath: '/home/user/projects/test-project',
|
|
12
|
+
remoteUrl: 'https://gitlab.com/test-org/test-project.git',
|
|
13
|
+
skill: 'review-front',
|
|
14
|
+
enabled: true,
|
|
15
|
+
};
|
|
16
|
+
vi.mock('@/config/loader.js', () => ({
|
|
17
|
+
loadConfig: vi.fn(() => mockConfig),
|
|
18
|
+
findRepositoryByProjectPath: vi.fn(() => mockRepoConfig),
|
|
19
|
+
}));
|
|
20
|
+
vi.mock('@/security/verifier.js', () => ({
|
|
21
|
+
verifyGitLabSignature: vi.fn(() => ({ valid: true })),
|
|
22
|
+
getGitLabEventType: vi.fn(() => 'Merge Request Hook'),
|
|
23
|
+
getGitLabEventUuid: vi.fn((request) => request.headers['x-gitlab-event-uuid']),
|
|
24
|
+
}));
|
|
25
|
+
vi.mock('@/frameworks/queue/pQueueAdapter.js', () => ({
|
|
26
|
+
createJobId: vi.fn(() => 'gitlab-test-org/test-project-42'),
|
|
27
|
+
enqueueReview: vi.fn(() => Promise.resolve(true)),
|
|
28
|
+
updateJobProgress: vi.fn(),
|
|
29
|
+
cancelJob: vi.fn(),
|
|
30
|
+
}));
|
|
31
|
+
vi.mock('@/claude/invoker.js', () => ({
|
|
32
|
+
invokeClaudeReview: vi.fn(),
|
|
33
|
+
sendNotification: vi.fn(),
|
|
34
|
+
}));
|
|
35
|
+
vi.mock('@/main/websocket.js', () => ({
|
|
36
|
+
startWatchingReviewContext: vi.fn(),
|
|
37
|
+
stopWatchingReviewContext: vi.fn(),
|
|
38
|
+
}));
|
|
39
|
+
vi.mock('@/config/projectConfig.js', () => ({
|
|
40
|
+
loadProjectConfig: vi.fn(() => null),
|
|
41
|
+
getProjectAgents: vi.fn(() => null),
|
|
42
|
+
getProjectAgentsOrFocusDefaults: vi.fn(() => null),
|
|
43
|
+
getFollowupAgents: vi.fn(() => null),
|
|
44
|
+
getProjectLanguage: vi.fn(() => 'en'),
|
|
45
|
+
}));
|
|
46
|
+
import { describe, it, expect, beforeEach } from 'vitest';
|
|
47
|
+
import { handleGitLabWebhook } from '../../modules/platform-integration/interface-adapters/controllers/webhook/gitlab.controller.js';
|
|
48
|
+
import { InMemoryIdempotencyStore } from '../../modules/platform-integration/interface-adapters/gateways/inMemoryIdempotencyStore.gateway.js';
|
|
49
|
+
import { CheckFollowupNeededUseCase } from '../../modules/tracking/usecases/tracking/checkFollowupNeeded.usecase.js';
|
|
50
|
+
import { HandlePlatformApprovalUseCase } from '../../modules/tracking/usecases/tracking/handlePlatformApproval.usecase.js';
|
|
51
|
+
import { RecordBypassUseCase } from '../../modules/tracking/usecases/tracking/recordBypass.usecase.js';
|
|
52
|
+
import { RecordPushUseCase } from '../../modules/tracking/usecases/tracking/recordPush.usecase.js';
|
|
53
|
+
import { RecordReviewCompletionUseCase } from '../../modules/tracking/usecases/tracking/recordReviewCompletion.usecase.js';
|
|
54
|
+
import { SyncThreadsUseCase } from '../../modules/tracking/usecases/tracking/syncThreads.usecase.js';
|
|
55
|
+
import { TrackAssignmentUseCase } from '../../modules/tracking/usecases/tracking/trackAssignment.usecase.js';
|
|
56
|
+
import { TransitionStateUseCase } from '../../modules/tracking/usecases/tracking/transitionState.usecase.js';
|
|
57
|
+
import { GitLabEventFactory } from '../../tests/factories/gitLabEvent.factory.js';
|
|
58
|
+
import { TrackedMrFactory } from '../../tests/factories/trackedMr.factory.js';
|
|
59
|
+
import { StubApprovalRevocationGateway } from '../../tests/stubs/approvalRevocation.stub.js';
|
|
60
|
+
import { StubIdempotencyStore } from '../../tests/stubs/idempotencyStore.stub.js';
|
|
61
|
+
import { createStubLogger } from '../../tests/stubs/logger.stub.js';
|
|
62
|
+
import { StubNoteCommentPostGateway } from '../../tests/stubs/noteCommentPost.stub.js';
|
|
63
|
+
class StubGateClaudeInvocation {
|
|
64
|
+
invocationCount = 0;
|
|
65
|
+
result;
|
|
66
|
+
constructor(result = {
|
|
67
|
+
status: 'enqueued',
|
|
68
|
+
jobId: 'gitlab-test-org/test-project-42',
|
|
69
|
+
}) {
|
|
70
|
+
this.result = result;
|
|
71
|
+
}
|
|
72
|
+
async execute(_input) {
|
|
73
|
+
this.invocationCount += 1;
|
|
74
|
+
return this.result;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function createMockTrackingGateway() {
|
|
78
|
+
const basicMr = TrackedMrFactory.create({
|
|
79
|
+
id: 'gitlab-test-org/test-project-42',
|
|
80
|
+
mrNumber: 42,
|
|
81
|
+
platform: 'gitlab',
|
|
82
|
+
project: 'test-org/test-project',
|
|
83
|
+
});
|
|
84
|
+
return {
|
|
85
|
+
getById: vi.fn(() => basicMr),
|
|
86
|
+
getByNumber: vi.fn(() => null),
|
|
87
|
+
create: vi.fn(),
|
|
88
|
+
update: vi.fn(),
|
|
89
|
+
getByState: vi.fn(() => []),
|
|
90
|
+
getActiveMrs: vi.fn(() => []),
|
|
91
|
+
remove: vi.fn(() => true),
|
|
92
|
+
archive: vi.fn(() => true),
|
|
93
|
+
recordReviewEvent: vi.fn(),
|
|
94
|
+
recordPush: vi.fn(() => null),
|
|
95
|
+
loadTracking: vi.fn(() => null),
|
|
96
|
+
saveTracking: vi.fn(),
|
|
97
|
+
};
|
|
98
|
+
}
|
|
99
|
+
function createStubContextGateway() {
|
|
100
|
+
return {
|
|
101
|
+
create: vi.fn(() => ({ success: true, filePath: '' })),
|
|
102
|
+
read: vi.fn(() => null),
|
|
103
|
+
delete: vi.fn(() => ({ success: true, deleted: true })),
|
|
104
|
+
exists: vi.fn(() => false),
|
|
105
|
+
getFilePath: vi.fn(() => ''),
|
|
106
|
+
appendAction: vi.fn(() => ({ success: true })),
|
|
107
|
+
updateProgress: vi.fn(() => ({ success: true })),
|
|
108
|
+
setResult: vi.fn(() => ({ success: true })),
|
|
109
|
+
listAll: vi.fn(() => []),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
function createAcceptAllEnforceBudget() {
|
|
113
|
+
return {
|
|
114
|
+
execute: vi.fn(async () => ({
|
|
115
|
+
accepted: true,
|
|
116
|
+
status: {
|
|
117
|
+
limitUsd: 200,
|
|
118
|
+
consumedUsd: 0,
|
|
119
|
+
remainingUsd: 200,
|
|
120
|
+
percentUsed: 0,
|
|
121
|
+
exceeded: false,
|
|
122
|
+
periodStart: '2026-05-01T00:00:00.000Z',
|
|
123
|
+
},
|
|
124
|
+
})),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
function createDeps(trackingGateway, overrides = {}) {
|
|
128
|
+
const threadFetchGateway = { fetchThreads: vi.fn(() => []) };
|
|
129
|
+
return {
|
|
130
|
+
reviewContextGateway: createStubContextGateway(),
|
|
131
|
+
threadFetchGateway,
|
|
132
|
+
diffMetadataFetchGateway: {
|
|
133
|
+
fetchDiffMetadata: vi.fn(() => ({ baseSha: 'abc', headSha: 'def', startSha: 'ghi' })),
|
|
134
|
+
},
|
|
135
|
+
diffStatsFetchGateway: { fetchDiffStats: vi.fn(() => null) },
|
|
136
|
+
trackAssignment: new TrackAssignmentUseCase(trackingGateway),
|
|
137
|
+
recordCompletion: new RecordReviewCompletionUseCase(trackingGateway),
|
|
138
|
+
recordPush: new RecordPushUseCase(trackingGateway),
|
|
139
|
+
transitionState: new TransitionStateUseCase(trackingGateway),
|
|
140
|
+
checkFollowupNeeded: new CheckFollowupNeededUseCase(trackingGateway),
|
|
141
|
+
syncThreads: new SyncThreadsUseCase(trackingGateway, threadFetchGateway),
|
|
142
|
+
enforceBudget: createAcceptAllEnforceBudget(),
|
|
143
|
+
broadcastBudgetExceeded: vi.fn(),
|
|
144
|
+
getRepositories: vi.fn(() => []),
|
|
145
|
+
removeWorktree: vi.fn(async () => ({ status: 'removed' })),
|
|
146
|
+
recordBypass: new RecordBypassUseCase(trackingGateway),
|
|
147
|
+
noteCommentPostGateway: new StubNoteCommentPostGateway(),
|
|
148
|
+
handlePlatformApproval: new HandlePlatformApprovalUseCase(trackingGateway),
|
|
149
|
+
approvalRevocationGateway: new StubApprovalRevocationGateway(),
|
|
150
|
+
getQualityThreshold: () => null,
|
|
151
|
+
now: () => '2026-05-26T12:00:00.000Z',
|
|
152
|
+
...overrides,
|
|
153
|
+
};
|
|
154
|
+
}
|
|
155
|
+
function requestWith(uuid) {
|
|
156
|
+
const headers = {};
|
|
157
|
+
if (uuid !== undefined) {
|
|
158
|
+
headers['x-gitlab-event-uuid'] = uuid;
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
body: GitLabEventFactory.createWithReviewerAdded('claude-bot'),
|
|
162
|
+
headers,
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
describe('Acceptance — SPEC-200: Webhook event idempotency and replay protection', () => {
|
|
166
|
+
let mockReply;
|
|
167
|
+
let mockGateway;
|
|
168
|
+
const logger = createStubLogger();
|
|
169
|
+
beforeEach(() => {
|
|
170
|
+
vi.clearAllMocks();
|
|
171
|
+
mockReply = {
|
|
172
|
+
status: vi.fn().mockReturnThis(),
|
|
173
|
+
send: vi.fn().mockReturnThis(),
|
|
174
|
+
};
|
|
175
|
+
mockGateway = createMockTrackingGateway();
|
|
176
|
+
mockGateway.getById.mockReturnValue(null);
|
|
177
|
+
});
|
|
178
|
+
describe('Rule: a redelivered event is acted upon at most once within the TTL window', () => {
|
|
179
|
+
it('answers the duplicate with HTTP 200 no-op, a single invocation and pristine output gateways', async () => {
|
|
180
|
+
const idempotencyStore = new InMemoryIdempotencyStore({ ttlMs: 60_000, clock: () => 0 });
|
|
181
|
+
const gateClaudeInvocation = new StubGateClaudeInvocation();
|
|
182
|
+
const noteCommentPostGateway = new StubNoteCommentPostGateway();
|
|
183
|
+
const approvalRevocationGateway = new StubApprovalRevocationGateway();
|
|
184
|
+
const trackAssignment = new TrackAssignmentUseCase(mockGateway);
|
|
185
|
+
const trackSpy = vi.spyOn(trackAssignment, 'execute');
|
|
186
|
+
const deps = createDeps(mockGateway, {
|
|
187
|
+
idempotencyStore,
|
|
188
|
+
gateClaudeInvocation,
|
|
189
|
+
noteCommentPostGateway,
|
|
190
|
+
approvalRevocationGateway,
|
|
191
|
+
trackAssignment,
|
|
192
|
+
});
|
|
193
|
+
await handleGitLabWebhook(requestWith('event-uuid-A'), mockReply, logger, mockGateway, deps);
|
|
194
|
+
const trackCallsAfterFirst = trackSpy.mock.calls.length;
|
|
195
|
+
await handleGitLabWebhook(requestWith('event-uuid-A'), mockReply, logger, mockGateway, deps);
|
|
196
|
+
expect(gateClaudeInvocation.invocationCount).toBe(1);
|
|
197
|
+
expect(trackSpy.mock.calls.length).toBe(trackCallsAfterFirst);
|
|
198
|
+
expect(noteCommentPostGateway.calls).toHaveLength(0);
|
|
199
|
+
expect(approvalRevocationGateway.calls).toHaveLength(0);
|
|
200
|
+
expect(mockReply.status).toHaveBeenLastCalledWith(200);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
describe('Rule: distinct events are independent', () => {
|
|
204
|
+
it('lets two different UUIDs each reach the invocation chokepoint', async () => {
|
|
205
|
+
const idempotencyStore = new InMemoryIdempotencyStore({ ttlMs: 60_000, clock: () => 0 });
|
|
206
|
+
const gateClaudeInvocation = new StubGateClaudeInvocation();
|
|
207
|
+
const deps = createDeps(mockGateway, { idempotencyStore, gateClaudeInvocation });
|
|
208
|
+
await handleGitLabWebhook(requestWith('event-uuid-A'), mockReply, logger, mockGateway, deps);
|
|
209
|
+
await handleGitLabWebhook(requestWith('event-uuid-B'), mockReply, logger, mockGateway, deps);
|
|
210
|
+
expect(gateClaudeInvocation.invocationCount).toBe(2);
|
|
211
|
+
});
|
|
212
|
+
});
|
|
213
|
+
describe('Rule: a missing event UUID degrades to gated, never hard-rejects', () => {
|
|
214
|
+
it('reaches the chokepoint once, records no dedup entry and exposes no rejection branch', async () => {
|
|
215
|
+
const idempotencyStore = new StubIdempotencyStore();
|
|
216
|
+
const gateClaudeInvocation = new StubGateClaudeInvocation();
|
|
217
|
+
const deps = createDeps(mockGateway, { idempotencyStore, gateClaudeInvocation });
|
|
218
|
+
await handleGitLabWebhook(requestWith(undefined), mockReply, logger, mockGateway, deps);
|
|
219
|
+
expect(gateClaudeInvocation.invocationCount).toBe(1);
|
|
220
|
+
expect(idempotencyStore.recordedKeys).toHaveLength(0);
|
|
221
|
+
expect(idempotencyStore.entryCount).toBe(0);
|
|
222
|
+
expect(mockReply.status).not.toHaveBeenCalledWith(400);
|
|
223
|
+
expect(mockReply.status).not.toHaveBeenCalledWith(409);
|
|
224
|
+
});
|
|
225
|
+
});
|
|
226
|
+
describe('Rule: an event redelivered after the TTL window is reprocessed', () => {
|
|
227
|
+
it('re-accepts the same UUID once the clock advances beyond the TTL', async () => {
|
|
228
|
+
let currentTimeMs = 0;
|
|
229
|
+
const idempotencyStore = new InMemoryIdempotencyStore({
|
|
230
|
+
ttlMs: 60_000,
|
|
231
|
+
clock: () => currentTimeMs,
|
|
232
|
+
});
|
|
233
|
+
const gateClaudeInvocation = new StubGateClaudeInvocation();
|
|
234
|
+
const deps = createDeps(mockGateway, { idempotencyStore, gateClaudeInvocation });
|
|
235
|
+
await handleGitLabWebhook(requestWith('event-uuid-A'), mockReply, logger, mockGateway, deps);
|
|
236
|
+
await handleGitLabWebhook(requestWith('event-uuid-A'), mockReply, logger, mockGateway, deps);
|
|
237
|
+
expect(gateClaudeInvocation.invocationCount).toBe(1);
|
|
238
|
+
currentTimeMs = 60_001;
|
|
239
|
+
await handleGitLabWebhook(requestWith('event-uuid-A'), mockReply, logger, mockGateway, deps);
|
|
240
|
+
expect(gateClaudeInvocation.invocationCount).toBe(2);
|
|
241
|
+
});
|
|
242
|
+
});
|
|
243
|
+
});
|
|
244
|
+
//# sourceMappingURL=200-webhook-event-idempotency.acceptance.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"200-webhook-event-idempotency.acceptance.test.js","sourceRoot":"","sources":["../../../src/tests/acceptance/200-webhook-event-idempotency.acceptance.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"201-transport-provenance-hardening.acceptance.test.d.ts","sourceRoot":"","sources":["../../../src/tests/acceptance/201-transport-provenance-hardening.acceptance.test.ts"],"names":[],"mappings":""}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { describe, it, expect } from 'vitest';
|
|
2
|
+
import { transportGuardMiddleware } from '../../modules/platform-integration/interface-adapters/controllers/webhook/transportGuard.middleware.js';
|
|
3
|
+
import { ForwardedForClientIpResolver } from '../../modules/platform-integration/interface-adapters/gateways/transport/clientIpResolver.forwardedFor.gateway.js';
|
|
4
|
+
const TRUSTED_HOP = '127.0.0.1';
|
|
5
|
+
const config = {
|
|
6
|
+
trustedHopAddress: TRUSTED_HOP,
|
|
7
|
+
allowedCidrRanges: ['10.20.30.0/24'],
|
|
8
|
+
};
|
|
9
|
+
class FakeResponse {
|
|
10
|
+
statusCode = null;
|
|
11
|
+
sent = false;
|
|
12
|
+
code(status) {
|
|
13
|
+
this.statusCode = status;
|
|
14
|
+
return this;
|
|
15
|
+
}
|
|
16
|
+
send() {
|
|
17
|
+
this.sent = true;
|
|
18
|
+
return this;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
function buildRequest(overrides = {}) {
|
|
22
|
+
return {
|
|
23
|
+
socket: { remoteAddress: overrides.remoteAddress ?? TRUSTED_HOP },
|
|
24
|
+
headers: {
|
|
25
|
+
'x-forwarded-proto': overrides.proto ?? 'https',
|
|
26
|
+
'x-forwarded-for': overrides.forwardedFor ?? '10.20.30.40, 127.0.0.1',
|
|
27
|
+
},
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function runGuard(request) {
|
|
31
|
+
let nextCalled = false;
|
|
32
|
+
const reply = new FakeResponse();
|
|
33
|
+
transportGuardMiddleware({
|
|
34
|
+
request,
|
|
35
|
+
reply,
|
|
36
|
+
next: () => {
|
|
37
|
+
nextCalled = true;
|
|
38
|
+
},
|
|
39
|
+
resolver: new ForwardedForClientIpResolver(),
|
|
40
|
+
}, config);
|
|
41
|
+
return { nextCalled, statusCode: reply.statusCode, sent: reply.sent };
|
|
42
|
+
}
|
|
43
|
+
describe('SPEC-201 transport provenance hardening (acceptance — full chokepoint transportGuardMiddleware)', () => {
|
|
44
|
+
it('AC3 + AC5: allowlisted, https, hop-trusted request reaches the handler via next() with no rejection', () => {
|
|
45
|
+
const outcome = runGuard(buildRequest());
|
|
46
|
+
expect(outcome.nextCalled).toBe(true);
|
|
47
|
+
expect(outcome.statusCode).toBeNull();
|
|
48
|
+
expect(outcome.sent).toBe(false);
|
|
49
|
+
});
|
|
50
|
+
it('AC1 + AC5: untrusted direct socket is rejected with 403 and the handler is never reached', () => {
|
|
51
|
+
const outcome = runGuard(buildRequest({ remoteAddress: '203.0.113.7' }));
|
|
52
|
+
expect(outcome.nextCalled).toBe(false);
|
|
53
|
+
expect(outcome.statusCode).toBe(403);
|
|
54
|
+
expect(outcome.sent).toBe(true);
|
|
55
|
+
});
|
|
56
|
+
it('AC2: a hop-trusted request whose forwarded protocol is not https is rejected with 403', () => {
|
|
57
|
+
const outcome = runGuard(buildRequest({ proto: 'http' }));
|
|
58
|
+
expect(outcome.nextCalled).toBe(false);
|
|
59
|
+
expect(outcome.statusCode).toBe(403);
|
|
60
|
+
});
|
|
61
|
+
it('AC4: a request whose resolved client ip is outside every configured cidr range is rejected with 403', () => {
|
|
62
|
+
const outcome = runGuard(buildRequest({ forwardedFor: '192.168.1.1' }));
|
|
63
|
+
expect(outcome.nextCalled).toBe(false);
|
|
64
|
+
expect(outcome.statusCode).toBe(403);
|
|
65
|
+
});
|
|
66
|
+
it('AC1 + AC6: a spoofed X-Forwarded-For cannot rescue an untrusted socket — header is ignored, request still rejected', () => {
|
|
67
|
+
const outcome = runGuard(buildRequest({ remoteAddress: '203.0.113.7', forwardedFor: '10.20.30.40' }));
|
|
68
|
+
expect(outcome.nextCalled).toBe(false);
|
|
69
|
+
expect(outcome.statusCode).toBe(403);
|
|
70
|
+
});
|
|
71
|
+
});
|
|
72
|
+
//# sourceMappingURL=201-transport-provenance-hardening.acceptance.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"201-transport-provenance-hardening.acceptance.test.js","sourceRoot":"","sources":["../../../src/tests/acceptance/201-transport-provenance-hardening.acceptance.test.ts"],"names":[],"mappings":"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"}
|
|
@@ -522,6 +522,47 @@ describe('handleGitLabWebhook', () => {
|
|
|
522
522
|
});
|
|
523
523
|
});
|
|
524
524
|
describe('AC4 - fail-closed membership resolution', () => {
|
|
525
|
+
function buildFollowupMr() {
|
|
526
|
+
return TrackedMrFactory.create({
|
|
527
|
+
id: 'gitlab-test-org/test-project-42',
|
|
528
|
+
mrNumber: 42,
|
|
529
|
+
platform: 'gitlab',
|
|
530
|
+
project: 'test-org/test-project',
|
|
531
|
+
state: 'pending-review',
|
|
532
|
+
openThreads: 3,
|
|
533
|
+
autoFollowup: true,
|
|
534
|
+
lastPushAt: '2026-05-26T12:00:00.000Z',
|
|
535
|
+
lastReviewAt: '2026-05-25T12:00:00.000Z',
|
|
536
|
+
});
|
|
537
|
+
}
|
|
538
|
+
function buildNoteEvent(note) {
|
|
539
|
+
return {
|
|
540
|
+
object_kind: 'note',
|
|
541
|
+
event_type: 'note',
|
|
542
|
+
user: { username: 'note-author', name: 'Note Author' },
|
|
543
|
+
project: {
|
|
544
|
+
id: 1,
|
|
545
|
+
name: 'test-project',
|
|
546
|
+
path_with_namespace: 'test-org/test-project',
|
|
547
|
+
web_url: 'https://gitlab.com/test-org/test-project',
|
|
548
|
+
git_http_url: 'https://gitlab.com/test-org/test-project.git',
|
|
549
|
+
},
|
|
550
|
+
object_attributes: {
|
|
551
|
+
id: 7,
|
|
552
|
+
note,
|
|
553
|
+
noteable_type: 'MergeRequest',
|
|
554
|
+
noteable_id: 99,
|
|
555
|
+
},
|
|
556
|
+
merge_request: {
|
|
557
|
+
iid: 42,
|
|
558
|
+
title: 'Test MR',
|
|
559
|
+
state: 'opened',
|
|
560
|
+
source_branch: 'feature/test',
|
|
561
|
+
target_branch: 'main',
|
|
562
|
+
url: 'https://gitlab.com/test-org/test-project/-/merge_requests/42',
|
|
563
|
+
},
|
|
564
|
+
};
|
|
565
|
+
}
|
|
525
566
|
it('parks a reviewer-added trigger when membership resolution throws', async () => {
|
|
526
567
|
mockGateway.getById.mockReturnValue(null);
|
|
527
568
|
const memberAccess = new StubMemberAccessGateway();
|
|
@@ -535,6 +576,37 @@ describe('handleGitLabWebhook', () => {
|
|
|
535
576
|
expect(enqueueReview).not.toHaveBeenCalled();
|
|
536
577
|
expect(pendingGateway.saveCount).toBe(1);
|
|
537
578
|
});
|
|
579
|
+
it('parks a followup trigger when membership resolution throws', async () => {
|
|
580
|
+
const followupMr = buildFollowupMr();
|
|
581
|
+
mockGateway.getById.mockImplementation(() => followupMr);
|
|
582
|
+
mockGateway.getByNumber.mockImplementation(() => followupMr);
|
|
583
|
+
mockGateway.recordPush.mockImplementation(() => followupMr);
|
|
584
|
+
const memberAccess = new StubMemberAccessGateway();
|
|
585
|
+
memberAccess.setShouldFail(true);
|
|
586
|
+
const pendingGateway = new StubPendingReviewRequestGateway();
|
|
587
|
+
const deps = buildGatedDeps(memberAccess, pendingGateway);
|
|
588
|
+
const event = GitLabEventFactory.createMrUpdate();
|
|
589
|
+
event.user = { username: 'dev-actor', name: 'Dev Actor' };
|
|
590
|
+
const request = { body: event, headers: {} };
|
|
591
|
+
await handleGitLabWebhook(request, mockReply, logger, mockGateway, deps);
|
|
592
|
+
expect(enqueueReview).not.toHaveBeenCalled();
|
|
593
|
+
expect(pendingGateway.saveCount).toBe(1);
|
|
594
|
+
});
|
|
595
|
+
it('parks a note trigger when membership resolution throws', async () => {
|
|
596
|
+
vi.mocked(getGitLabEventType).mockReturnValueOnce('Note Hook');
|
|
597
|
+
const memberAccess = new StubMemberAccessGateway();
|
|
598
|
+
memberAccess.setShouldFail(true);
|
|
599
|
+
const pendingGateway = new StubPendingReviewRequestGateway();
|
|
600
|
+
const deps = buildGatedDeps(memberAccess, pendingGateway);
|
|
601
|
+
const request = {
|
|
602
|
+
body: buildNoteEvent('/bypass-quality "reason here"'),
|
|
603
|
+
headers: {},
|
|
604
|
+
};
|
|
605
|
+
await handleGitLabWebhook(request, mockReply, logger, mockGateway, deps);
|
|
606
|
+
expect(mockReply.status).toHaveBeenCalledWith(202);
|
|
607
|
+
expect(mockReply.send).toHaveBeenCalledWith(expect.objectContaining({ status: 'pending-confirmation', reason: 'untrusted-actor' }));
|
|
608
|
+
expect(mockGateway.update).not.toHaveBeenCalled();
|
|
609
|
+
});
|
|
538
610
|
});
|
|
539
611
|
});
|
|
540
612
|
describe('extractBaseUrl', () => {
|
|
@@ -561,6 +633,18 @@ describe('handleGitLabWebhook', () => {
|
|
|
561
633
|
expect(mockReply.send).toHaveBeenCalledWith({ error: 'bad-token' });
|
|
562
634
|
expect(enqueueReview).not.toHaveBeenCalled();
|
|
563
635
|
});
|
|
636
|
+
it('never queries the membership gateway when the token is invalid (SPEC-197 AC6)', async () => {
|
|
637
|
+
vi.mocked(verifyGitLabSignature).mockReturnValueOnce({ valid: false, error: 'bad-token' });
|
|
638
|
+
const memberAccess = new StubMemberAccessGateway();
|
|
639
|
+
memberAccess.setAccess('dev-actor', MEMBER_ACCESS_LEVELS.developer);
|
|
640
|
+
const deps = { ...defaultDeps, isTrustedActor: new IsTrustedActorUseCase(memberAccess) };
|
|
641
|
+
const event = GitLabEventFactory.createWithReviewerAdded('claude-bot');
|
|
642
|
+
event.user = { username: 'dev-actor', name: 'Dev Actor' };
|
|
643
|
+
const request = { body: event, headers: {} };
|
|
644
|
+
await handleGitLabWebhook(request, mockReply, logger, mockGateway, deps);
|
|
645
|
+
expect(mockReply.status).toHaveBeenCalledWith(401);
|
|
646
|
+
expect(memberAccess.calls.length).toBe(0);
|
|
647
|
+
});
|
|
564
648
|
it('ignores events that are neither Note Hook nor Merge Request Hook', async () => {
|
|
565
649
|
vi.mocked(getGitLabEventType).mockReturnValueOnce('Pipeline Hook');
|
|
566
650
|
const event = GitLabEventFactory.createWithReviewerAdded('claude-bot');
|
|
@@ -683,6 +767,18 @@ describe('handleGitLabWebhook', () => {
|
|
|
683
767
|
expect(mockReply.send).toHaveBeenCalledWith(expect.objectContaining({ status: 'pending-confirmation', reason: 'untrusted-actor' }));
|
|
684
768
|
expect(mockGateway.update).not.toHaveBeenCalled();
|
|
685
769
|
});
|
|
770
|
+
it('lets a note trigger from a Developer actor proceed past the provenance gate (AC3 positive)', async () => {
|
|
771
|
+
const memberAccess = new StubMemberAccessGateway();
|
|
772
|
+
memberAccess.setAccess('note-author', MEMBER_ACCESS_LEVELS.developer);
|
|
773
|
+
const deps = { ...defaultDeps, isTrustedActor: new IsTrustedActorUseCase(memberAccess) };
|
|
774
|
+
const request = {
|
|
775
|
+
body: buildNoteEvent('/bypass-quality "reason here"'),
|
|
776
|
+
headers: {},
|
|
777
|
+
};
|
|
778
|
+
await handleGitLabWebhook(request, mockReply, logger, mockGateway, deps);
|
|
779
|
+
expect(mockReply.status).not.toHaveBeenCalledWith(202);
|
|
780
|
+
expect(mockReply.send).toHaveBeenCalledWith(expect.objectContaining({ status: 'bypass-recorded' }));
|
|
781
|
+
});
|
|
686
782
|
});
|
|
687
783
|
describe('closed MR with unconfigured repository', () => {
|
|
688
784
|
it('acknowledges without cleanup when the repo is not configured', async () => {
|