Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 10 additions & 10 deletions apps/api/src/ipns/ipns.service.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -99,14 +99,14 @@ describe('IpnsService', () => {

it('should publish record for new folder successfully', async () => {
mockFolderIpnsRepo.findOne.mockResolvedValue(null);
mockFolderIpnsRepo.create.mockReturnValue({ ...mockFolderEntity, sequenceNumber: '0' });
mockFolderIpnsRepo.save.mockResolvedValue({ ...mockFolderEntity, sequenceNumber: '0' });
mockFolderIpnsRepo.create.mockReturnValue({ ...mockFolderEntity, sequenceNumber: '1' });
mockFolderIpnsRepo.save.mockResolvedValue({ ...mockFolderEntity, sequenceNumber: '1' });

const result = await service.publishRecord(testUserId, createDto());

expect(result.success).toBe(true);
expect(result.ipnsName).toBe(testIpnsName);
expect(result.sequenceNumber).toBe('0');
expect(result.sequenceNumber).toBe('1');
expect(mockDelegatedRoutingClient.publish).toHaveBeenCalledWith(
testIpnsName,
expect.any(Uint8Array)
Expand Down Expand Up @@ -141,13 +141,13 @@ describe('IpnsService', () => {
mockFolderIpnsRepo.findOne.mockResolvedValue(null);
mockFolderIpnsRepo.create.mockReturnValue({
...mockFolderEntity,
sequenceNumber: '0',
sequenceNumber: '1',
encryptedIpnsPrivateKey: null,
keyEpoch: null,
});
mockFolderIpnsRepo.save.mockResolvedValue({
...mockFolderEntity,
sequenceNumber: '0',
sequenceNumber: '1',
encryptedIpnsPrivateKey: null,
keyEpoch: null,
});
Expand All @@ -169,13 +169,13 @@ describe('IpnsService', () => {
mockFolderIpnsRepo.findOne.mockResolvedValue(null);
mockFolderIpnsRepo.create.mockReturnValue({
...mockFolderEntity,
sequenceNumber: '0',
sequenceNumber: '1',
encryptedIpnsPrivateKey: Buffer.from(testEncryptedIpnsPrivateKey, 'hex'),
keyEpoch: null,
});
mockFolderIpnsRepo.save.mockResolvedValue({
...mockFolderEntity,
sequenceNumber: '0',
sequenceNumber: '1',
encryptedIpnsPrivateKey: Buffer.from(testEncryptedIpnsPrivateKey, 'hex'),
keyEpoch: null,
});
Expand Down Expand Up @@ -314,7 +314,7 @@ describe('IpnsService', () => {
userId: testUserId,
ipnsName: testIpnsName,
latestCid: testMetadataCid,
sequenceNumber: '0',
sequenceNumber: '1',
encryptedIpnsPrivateKey: Buffer.from(testEncryptedIpnsPrivateKey, 'hex'),
keyEpoch: testKeyEpoch,
isRoot: false,
Expand All @@ -335,7 +335,7 @@ describe('IpnsService', () => {
userId: testUserId,
ipnsName: testIpnsName,
latestCid: testMetadataCid,
sequenceNumber: '0',
sequenceNumber: '1',
encryptedIpnsPrivateKey: Buffer.from(testEncryptedIpnsPrivateKey, 'hex'),
keyEpoch: testKeyEpoch,
isRoot: false,
Expand Down Expand Up @@ -852,7 +852,7 @@ describe('IpnsService', () => {
mockFolderIpnsRepo.create.mockImplementation((data) => ({
...data,
id: 'new-id',
sequenceNumber: '0',
sequenceNumber: '1',
}));
mockFolderIpnsRepo.save.mockImplementation((entity) => Promise.resolve({ ...entity }));

Expand Down
5 changes: 3 additions & 2 deletions apps/api/src/ipns/ipns.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -224,12 +224,13 @@ export class IpnsService {
return saved;
}

// Create new entry
// Create new entry — sequence starts at '1' to match the IPNS record
// the client signed (clients compute newSeq = 0n + 1n = 1n for first publish).
const folder = this.folderIpnsRepository.create({
userId,
ipnsName,
latestCid: metadataCid,
sequenceNumber: '0',
sequenceNumber: '1',
Comment thread
coderabbitai[bot] marked this conversation as resolved.
encryptedIpnsPrivateKey: encryptedIpnsPrivateKey
? Buffer.from(encryptedIpnsPrivateKey, 'hex')
: null,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { MigrationInterface, QueryRunner } from 'typeorm';

/**
* Migration: Backfill IPNS sequence numbers from '0' to '1'
*
* Before this fix, upsertFolderIpns created new entries with
* sequenceNumber='0' while the client stored sequenceNumber=1n
* (computing 0+1=1 after the first publish). Phase 16 added
* conflict detection via expectedSequenceNumber, exposing this
* off-by-one mismatch as 409 errors.
*
* This migration bumps non-root entries that have published content
* (latestCid IS NOT NULL) from '0' to '1' to align with what
* the client already stored.
*
* Root entries (created during vault init with latestCid=null) are
* excluded because their first actual publish goes through the
* UPDATE path which correctly increments.
*/
export class BackfillIpnsSequenceNumbers1740500000000 implements MigrationInterface {
name = 'BackfillIpnsSequenceNumbers1740500000000';

public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`
UPDATE "folder_ipns"
SET "sequence_number" = '1'
WHERE "sequence_number" = '0'
AND "latest_cid" IS NOT NULL
AND "is_root" = false
`);
}

public async down(_queryRunner: QueryRunner): Promise<void> {
// Not reversible — we can't distinguish which rows were backfilled
// vs. which legitimately had sequence_number='1' before.
// This is a data-only migration with no schema changes.
}
}