Skip to content

Commit bf8d589

Browse files
committed
fix(files): validate the revert revision before the no-op branch and omit an absent revision
1 parent 126cf2a commit bf8d589

7 files changed

Lines changed: 59 additions & 32 deletions

File tree

‎apps/docs/openapi-v2-files-audit.json‎

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -5321,15 +5321,8 @@
53215321
"description": "Current public-share state, or null when the file has never been shared."
53225322
},
53235323
"revision": {
5324-
"anyOf": [
5325-
{
5326-
"type": "string"
5327-
},
5328-
{
5329-
"type": "null"
5330-
}
5331-
],
5332-
"description": "Opaque token for the file's current content. Send it back as `expectedRevision` so a write or revert is refused when the content moved on. Null for a file with no recorded content version."
5324+
"description": "Opaque token for the file's current content. Send it back as `expectedRevision` so a write or revert is refused when the content moved on. Absent for a file with no recorded content version.",
5325+
"type": "string"
53335326
},
53345327
"currentVersion": {
53355328
"type": "integer",
@@ -5351,7 +5344,6 @@
53515344
"updatedAt",
53525345
"deletedAt",
53535346
"share",
5354-
"revision",
53555347
"currentVersion"
53565348
],
53575349
"additionalProperties": false,
@@ -5386,7 +5378,7 @@
53865378
"deletedAt": null,
53875379
"share": null,
53885380
"currentVersion": 1,
5389-
"revision": "d2ZfNGtKOW1OMnBRN3JTOjIwMjYtMDEtMTVUMTA6MzA6MDAuMDAwWg"
5381+
"revision": "d2ZfVjFTdEdYUjh6NWpkSGk2Qm15VDkxOjIwMjYtMDEtMTVUMTA6MzA6MDAuMDAwWg"
53905382
}
53915383
},
53925384
{
@@ -5414,7 +5406,7 @@
54145406
"allowedEmails": []
54155407
},
54165408
"currentVersion": 3,
5417-
"revision": "d2ZfNGtKOW1OMnBRN3JTOjIwMjYtMDEtMTZUMDk6MTI6MDAuMDAwWg"
5409+
"revision": "d2ZfVjFTdEdYUjh6NWpkSGk2Qm15VDkxOjIwMjYtMDEtMTZUMDk6MTI6MDAuMDAwWg"
54185410
}
54195411
}
54205412
]

‎apps/sim/app/api/v2/files/[fileId]/metadata/route.ts‎

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -30,12 +30,15 @@ export const GET = defineV2JsonRoute({
3030
includeDeleted: query.scope === 'archived',
3131
}),
3232
useCase: readWorkspaceFileMetadataWithVersion,
33-
present: async ({ file, share }) => ({
34-
data: {
35-
...(await toV2File(file)),
36-
share,
37-
currentVersion: file.currentVersion,
38-
revision: workspaceFileRevision(file),
39-
},
40-
}),
33+
present: async ({ file, share }) => {
34+
const revision = workspaceFileRevision(file)
35+
return {
36+
data: {
37+
...(await toV2File(file)),
38+
share,
39+
currentVersion: file.currentVersion,
40+
...(revision === null ? {} : { revision }),
41+
},
42+
}
43+
},
4144
})

‎apps/sim/lib/api/contracts/v2/files.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -158,9 +158,9 @@ export const v2FileMetadataSchema = v2FileSchema
158158
.describe('Current public-share state, or null when the file has never been shared.'),
159159
revision: z
160160
.string()
161-
.nullable()
161+
.optional()
162162
.describe(
163-
"Opaque token for the file's current content. Send it back as `expectedRevision` so a write or revert is refused when the content moved on. Null for a file with no recorded content version."
163+
"Opaque token for the file's current content. Send it back as `expectedRevision` so a write or revert is refused when the content moved on. Absent for a file with no recorded content version."
164164
),
165165
currentVersion: versionNumberSchema.describe(
166166
'Version number of the current content. List File Versions returns the history; pass this as `expectedCurrentVersion` to revert only if nothing changed since.'

‎apps/sim/lib/api/contracts/v2/openapi/files-audit.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -876,15 +876,15 @@ const declaredRoutes = [
876876
...FILE_EXAMPLE,
877877
share: null,
878878
currentVersion: 1,
879-
revision: 'd2ZfNGtKOW1OMnBRN3JTOjIwMjYtMDEtMTVUMTA6MzA6MDAuMDAwWg',
879+
revision: 'd2ZfVjFTdEdYUjh6NWpkSGk2Qm15VDkxOjIwMjYtMDEtMTVUMTA6MzA6MDAuMDAwWg',
880880
},
881881
},
882882
{
883883
data: {
884884
...FILE_EXAMPLE,
885885
share: SHARE_EXAMPLE,
886886
currentVersion: 3,
887-
revision: 'd2ZfNGtKOW1OMnBRN3JTOjIwMjYtMDEtMTZUMDk6MTI6MDAuMDAwWg',
887+
revision: 'd2ZfVjFTdEdYUjh6NWpkSGk2Qm15VDkxOjIwMjYtMDEtMTZUMDk6MTI6MDAuMDAwWg',
888888
},
889889
},
890890
]

‎apps/sim/lib/workspace-files/application/file-versions.test.ts‎

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,7 @@ describe('file version use cases', () => {
211211
})
212212

213213
/** The revision guards content, so it catches an edit that folded into the current version. */
214-
it('guards the revert with the revision the caller read', async () => {
215-
const callerRevision = new Date('2026-01-02T00:00:00Z')
214+
it('reverts when the revision still names the current content', async () => {
216215
mocks.getVersion.mockImplementation(async (_file: unknown, number: number) =>
217216
number === 2 ? version(2) : number === 4 ? version(4, { isCurrent: true }) : current
218217
)
@@ -223,15 +222,34 @@ describe('file version use cases', () => {
223222
fileId: 'file-1',
224223
assertedWorkspaceId: 'workspace-1',
225224
version: 2,
226-
expectedRevision: workspaceFileRevision({ ...file, contentUpdatedAt: callerRevision }),
225+
expectedRevision: workspaceFileRevision(file),
227226
},
228227
})
229228

230229
expect(mocks.updateContent.mock.calls[0][5]).toMatchObject({
231-
expectedUpdatedAt: callerRevision,
230+
expectedUpdatedAt: file.contentUpdatedAt,
232231
})
233232
})
234233

234+
/** Reverting to the version that is already current must still honour a stale revision. */
235+
it('refuses a stale revision even when the requested version is already current', async () => {
236+
await expect(
237+
revertWorkspaceFileVersion.execute({
238+
principal,
239+
input: {
240+
fileId: 'file-1',
241+
assertedWorkspaceId: 'workspace-1',
242+
version: 3,
243+
expectedRevision: workspaceFileRevision({
244+
...file,
245+
contentUpdatedAt: new Date('2020-01-01T00:00:00Z'),
246+
}),
247+
},
248+
})
249+
).rejects.toMatchObject({ code: 'conflict' })
250+
expect(mocks.updateContent).not.toHaveBeenCalled()
251+
})
252+
235253
it('refuses a revision issued for a different file', async () => {
236254
await expect(
237255
revertWorkspaceFileVersion.execute({

‎apps/sim/lib/workspace-files/application/file-versions.ts‎

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,22 @@ async function executeRevertWorkspaceFileVersion({
236236
`The current version is ${current.version}, not ${input.expectedCurrentVersion}`
237237
)
238238
}
239+
/*
240+
* Checked before the no-op branch below: a caller that named content which has since changed
241+
* must hear about it, not be told there was nothing to do.
242+
*/
243+
const expectedContentAt = input.expectedRevision
244+
? parseWorkspaceFileRevision(input.expectedRevision, context.fileId)
245+
: undefined
246+
if (
247+
expectedContentAt &&
248+
(file.contentUpdatedAt ?? file.updatedAt).getTime() !== expectedContentAt.getTime()
249+
) {
250+
throw new OrchestrationError(
251+
'conflict',
252+
'The file changed since the revision you read; re-read it before reverting'
253+
)
254+
}
239255
const target = await loadVersion(file, input.version)
240256
if (target.isCurrent) {
241257
return { file, version: target, reverted: false, revertedFrom: current.version }
@@ -272,9 +288,7 @@ async function executeRevertWorkspaceFileVersion({
272288
source: 'revert',
273289
restoredFromVersion: target.version,
274290
}),
275-
expectedUpdatedAt: input.expectedRevision
276-
? parseWorkspaceFileRevision(input.expectedRevision, context.fileId)
277-
: (file.contentUpdatedAt ?? file.updatedAt),
291+
expectedUpdatedAt: expectedContentAt ?? file.contentUpdatedAt ?? file.updatedAt,
278292
secretProvenancePolicy: { mode: 'reinstate', snapshot: provenance },
279293
}
280294
)

‎packages/sim-cli/src/generated/v2-api.ts‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4162,7 +4162,7 @@ type GetFileResponseRef1 = {
41624162
updatedAt: string
41634163
deletedAt: string | null
41644164
share: GetFileResponseRef0 | null
4165-
revision: string | null
4165+
revision?: string
41664166
currentVersion: number
41674167
}
41684168

0 commit comments

Comments
 (0)