diff --git a/docs/locks.md b/docs/locks.md
index 2ac71d6b46..80d5ce79a4 100644
--- a/docs/locks.md
+++ b/docs/locks.md
@@ -17,18 +17,18 @@ A lock post looks like a normal post (a short / image / link / … teaser) with
const isLock = !!postDetails.lock; // PostContentBase.tsx
```
-`lock` is **provisional** on `NexusPostDetails` / `PostDetailsModel` — Nexus and
-pubky-app-specs do not send it yet (only the local demo seeds it). Tagged
-`TODO:[Locks] #1998`; remove the note once the spec / Nexus deliver it.
+`lock` is written by the publish flow (`useCreateLockContent` → `services/local/post`) and
+persisted on `NexusPostDetails` / `PostDetailsModel`. Whether Nexus serves it back on read is
+not confirmed here.
## Data shape
-| Field | Owner | Meaning |
-| ------------------------ | ------------------------------------------ | --------------------------------------------------------------------------------- |
-| top-level `kind` | Nexus / specs | teaser display type (`image` / `link` / `short` / …; never `long` / `collection`) |
-| top-level `lock` | Lock server (provisional; FE-mock for now) | URL of the public `lock.json` — the detection seam |
-| `content` (string) | **FE-owned** | stringified teaser JSON, Zod-validated: `lock_title`, `teaser_description` |
-| `lock.json` → `LockFile` | **Lock server** | the public content-lock contract (see below) |
+| Field | Owner | Meaning |
+| ------------------------ | --------------- | --------------------------------------------------------------------------------- |
+| top-level `kind` | Nexus / specs | teaser display type (`image` / `link` / `short` / …; never `long` / `collection`) |
+| top-level `lock` | Lock server | URL of the public `lock.json` — the detection seam |
+| `content` (string) | **FE-owned** | stringified teaser JSON, Zod-validated: `lock_title`, `teaser_description` |
+| `lock.json` → `LockFile` | **Lock server** | the public content-lock contract (see below) |
- `content` is FE-owned (pubky-app-specs does not manage it) and validated at runtime
with Zod (`lockPostContentSchema`, `core/services/locks/locks.types.ts`). Bad / missing
@@ -36,7 +36,7 @@ pubky-app-specs do not send it yet (only the local demo seeds it). Tagged
- `LockFile` mirrors the Lock server's public `lock.json` (`version`, `creator`,
`primary_resource`, `secondary_resources`, `criteria`, `lock_logic`, `access_policy`,
`lock_server`). It is the **Lock server's contract**, not FE-owned — it should come from
- the Lock SDK once that ships a typed reader API (`TODO:[Locks] #1998`). Until then it is
+ the Lock SDK once that exports one (`TODO:[Locks] locks#22`). Until then it is
hand-mirrored in `locks.types.ts`.
## Render flow (shared by feed and detail)
@@ -112,23 +112,16 @@ Notes:
factory logs + reports to Sentry once). Hooks catch and degrade to the lock card.
- **Read path.** This is a read flow; there is no local-first `commit*` write.
-## Release-gate markers
+## Marker tracking
-Every dev / temporary shortcut is tagged so it can be audited out before ship. The gate
-(#2040): `grep -rn "TODO:\[Locks\]" src/` must return **zero** before release.
-
-| Marker | Removed when | Examples |
-| -------------------- | ---------------------------------------------- | ----------------------------------------------------------------------------- |
-| `TODO:[Locks] #1998` | Phase-1 (password-only) / provisional resolved | provisional `.lock` field; `LockFile` type → Lock SDK; inline test fixtures |
-| `TODO:[Locks] #2001` | auth sandbox verified against the live flow | the `/connect` iframe sandbox set |
-| `TODO:[Locks] #2039` | upload robustness landed | orphaned resources after a partial publish; no size checks before upload |
-| `TODO:[Locks] #2040` | Lock Server ships the password verifier | `dev-static` proofs (every criterion passes); unvalidated `any` from lock-sdk |
-| `TODO:[Locks] #2181` | announcement failure rolls the lock back | a lock left unreferenced when its announcement post fails |
+Every dev / temporary shortcut carries the ticket number that owns it —
+`grep -rn "TODO:\[Locks\]" src/` lists them, and each number is the issue to read.
+Use `grep -rniE "TODO.*lock" src/` to catch one that lost its tag.
## Testing & local demo
-- Tests are co-located with each file. Sample test data (a `LockFile` + an author pubky)
- is **inlined per test** — there is no mock-data module.
+- Tests are co-located with each file. Shared sample data (a `LockFile` + an author pubky)
+ lives in `src/test-utils/locks.ts` (`mockLockFile()`, `MOCK_LOCK_AUTHOR_PUBKY`).
- No integration test spans UI → application → SDK for the unlock flow; the local stack
(testnet + Lock Server + nexus) is driven manually.
diff --git a/src/components/molecules/DialogUnlockContent/DialogUnlockContent.tsx b/src/components/molecules/DialogUnlockContent/DialogUnlockContent.tsx
index fcf45d8db0..da4951b100 100644
--- a/src/components/molecules/DialogUnlockContent/DialogUnlockContent.tsx
+++ b/src/components/molecules/DialogUnlockContent/DialogUnlockContent.tsx
@@ -42,8 +42,7 @@ export function DialogUnlockContent({
onOpenChange(next);
};
- // Gate is only "non-empty": Phase 1 has no server password verifier (dev-static passes everything).
- // TODO:[Locks] #2040 — enforce the real password once it lands.
+ // TODO:[Locks] #2369 — password and `dev-static` all go away here.
const handleSubmit = () => {
if (!password || loading) return;
onSubmit(password);
diff --git a/src/components/molecules/LockedPostCard/LockedPostCard.tsx b/src/components/molecules/LockedPostCard/LockedPostCard.tsx
index 228c82305e..2d7ba16687 100644
--- a/src/components/molecules/LockedPostCard/LockedPostCard.tsx
+++ b/src/components/molecules/LockedPostCard/LockedPostCard.tsx
@@ -123,8 +123,8 @@ export function LockedPostCard({
)}
- {/* TODO:[Locks] #1998 — Phase 1 is password-only, so the indicator is hardcoded. The payment
- variant (price, from `verifierType`) arrives with the payment verifier. */}
+ {/* TODO:[Locks] #2369 — password and `dev-static` all go away here; the price variant reads
+ from `verifierType` instead. */}
diff --git a/src/components/organisms/LockedPostContent/LockedPostContent.tsx b/src/components/organisms/LockedPostContent/LockedPostContent.tsx
index e74a42e37c..7b71abec61 100644
--- a/src/components/organisms/LockedPostContent/LockedPostContent.tsx
+++ b/src/components/organisms/LockedPostContent/LockedPostContent.tsx
@@ -51,8 +51,6 @@ export function LockedPostContent({
const tToast = useTranslations('toast.post');
const tLock = useTranslations('post.lock');
- // TODO:[Locks] #1998 — `lockContent` is null when the teaser content can't be parsed. Rendering
- // nothing matches the previous behaviour; the unparseable-lock UX is still undecided.
if (!lockContent) return null;
const handleViewContent = async (password: string) => {
diff --git a/src/core/application/locks/locks.ts b/src/core/application/locks/locks.ts
index 00a3e45665..93097b9f94 100644
--- a/src/core/application/locks/locks.ts
+++ b/src/core/application/locks/locks.ts
@@ -37,14 +37,7 @@ const MAX_POLL_ATTEMPTS = 40;
const isVerifying = (status: TVerificationStatus) => status === 'pending' || status === 'in_progress';
-// TODO:[Locks] #2040 — Phase 1 ships the `password` verifier, but the Lock Server does not implement
-// one yet: `VerifierType` (locks-core/src/lock_policy.rs) only has `DevStatic`, and unknown verifier
-// strings are rejected while parsing, so `password` cannot be sent today. `dev-static` is a
-// placeholder that always satisfies — it MUST be replaced (and this constant deleted) before ship.
-// Blocked on the Lock Server adding a password verifier.
-//
-// When it lands: the public lock file (`/pub/locks.app/.json`) carries `criteria[].params`
-// verbatim, so the creator's password must never be put there in plaintext.
+// TODO:[Locks] #2369 — password and `dev-static` all go away here.
const CRITERION_ID = 'criterion-1';
const VERIFIER_TYPE = 'dev-static';
const VERIFIER_PARAMS = { satisfied: true };
@@ -160,10 +153,8 @@ export class LocksApplication {
* A validation error (permanent data fault) drops only that attachment; any other failure rejects
* the whole read so no caller persists a partial result — see the catch below.
*
- * TODO:[Locks] #2040 — both failure kinds surface only after the unlock is already paid for (credential
- * issued), so the reader needs a user-facing retry UI that does not charge again — re-download for
- * transient failures, re-fetch of the dropped attachment for permanent ones. Decide with the real
- * payment verifier.
+ * TODO:[Locks] #2374 — a permanently dropped attachment still lets the marker land, so the lock
+ * reads as fully unlocked and the attachment is unrecoverable.
*/
private static async readAttachments(
lockFile: LockFile,
@@ -175,6 +166,9 @@ export class LocksApplication {
uris.map(async (uri) => {
try {
const path = GuardedContentParser.attachmentUriToPath(uri);
+ // TODO:[Locks] locks#10 — bytes come from the homeserver but the type comes from
+ // the public `lock.json`, so the two can disagree. Both reads return bytes only today; take
+ // the type from the response header once the SDK exposes it.
const contentType = lockFile.secondary_resources?.[path]?.content_type;
// No descriptor = a permanent data-integrity error (the bytes live on a HS with no content
// type, so they can never render). Report to Sentry, then drop this one attachment.
@@ -282,10 +276,6 @@ export class LocksApplication {
* (`/priv/locks.app/content/`) — no unlock, no credential, no replication.
* Only valid when the lock owner is the signed-in account (a == b); the caller
* verifies that before calling.
- *
- * TODO:[Locks] #1998 — content types still come from the public `lock.json` (`secondary_resources`),
- * since direct-read/proxy-read return bytes only. pubky/locks#25 makes the SDK preserve the
- * response's content-type header; once it's integrated, read the type from there and drop this.
*/
static async fetchOwnContent({ lockFile }: TFetchOwnContentParams): Promise {
const primaryPath = lockFile.primary_resource?.path;
@@ -398,7 +388,6 @@ export class LocksApplication {
});
}
- // TODO:[Locks] #2040 — lock-sdk returns `any`; validate with Zod instead of casting.
return (await LocksService.readContentLock(lockUrl)) as LockFile;
}
}
diff --git a/src/core/controllers/locks/locks.test.ts b/src/core/controllers/locks/locks.test.ts
index e3b601afd6..e5d1e14ac1 100644
--- a/src/core/controllers/locks/locks.test.ts
+++ b/src/core/controllers/locks/locks.test.ts
@@ -4,9 +4,10 @@ import { LocksApplication } from '@/application/locks/locks';
import { AuthErrorCode } from '@/libs/error/error.codes';
import { Err } from '@/libs/error/error.factories';
import { ErrorService } from '@/libs/error/error.types';
-import { type LockFile, VerifierType } from '@/services/locks/locks.types';
+import { VerifierType } from '@/services/locks/locks.types';
import { useLocksAuthStore } from '@/stores/locksAuth/locksAuth.store';
import { locksAuthInitialState } from '@/stores/locksAuth/locksAuth.types';
+import { MOCK_LOCK_AUTHOR_PUBKY, mockLockFile } from '@/test-utils/locks';
import { asOpaque } from '@/test-utils/type-assertions';
import { LocksController } from './locks';
@@ -251,24 +252,7 @@ describe('LocksController (content)', () => {
});
});
-// TODO:[Locks] #1998 — inline test fixtures (sample lock file + author pubky) are
-// duplicated across the lock tests; consider extracting a shared test util/fixture.
-const MOCK_LOCK_AUTHOR_PUBKY = 'qr3xqyz3e5cyf9npgxc5zfp15ehhcis6gqsxob4une7bwwazekry';
-const MOCK_LOCK_FILE: LockFile = {
- version: 1,
- creator: 'pubkycreator123',
- primary_resource: {
- path: '/priv/locks.app/content/example.txt',
- hash: '',
- content_type: 'text/plain',
- size: 13,
- },
- secondary_resources: {},
- criteria: [{ criterion_id: 'criterion-1', verifier_type: 'password', params: { satisfied: true } }],
- lock_logic: { type: 'all', criteria: ['criterion-1'] },
- access_policy: { requested_credential_ttl_seconds: 900 },
- lock_server: { override: 'pubkyserver123' },
-};
+const MOCK_LOCK_FILE = mockLockFile();
const VALID_LOCK_URL = `pubky://${MOCK_LOCK_AUTHOR_PUBKY}/pub/locks/lock.json`;
diff --git a/src/core/models/post/details/postDetails.ts b/src/core/models/post/details/postDetails.ts
index 03590bdefa..c98755e228 100644
--- a/src/core/models/post/details/postDetails.ts
+++ b/src/core/models/post/details/postDetails.ts
@@ -15,9 +15,6 @@ export class PostDetailsModel
kind: string;
uri: string;
attachments: string[] | null;
- // TODO:[Locks] #1998 — top-level lock URL; see NexusPostDetails.lock. Optional
- // (matches the schema), but assigned in the constructor so `new PostDetailsModel(row)`
- // (findById) doesn't drop it on read.
lock?: string | null;
constructor(postDetails: PostDetailsModelSchema) {
diff --git a/src/core/pipes/locks/locks.parser.test.ts b/src/core/pipes/locks/locks.parser.test.ts
index b366210d9e..1019927d94 100644
--- a/src/core/pipes/locks/locks.parser.test.ts
+++ b/src/core/pipes/locks/locks.parser.test.ts
@@ -1,25 +1,9 @@
import { describe, expect, it } from 'vitest';
-import { type GuardedPost, type LockFile, VerifierType } from '@/services/locks/locks.types';
+import { type GuardedPost, VerifierType } from '@/services/locks/locks.types';
+import { MOCK_LOCK_AUTHOR_PUBKY, mockLockFile } from '@/test-utils/locks';
import { GuardedContentParser, LockContentParser, LockFileParser, LockProofBundler } from './locks.parser';
-// TODO:[Locks] #1998 — inline test fixtures (sample lock file + author pubky) are
-// duplicated across the lock tests; consider extracting a shared test util/fixture.
-const MOCK_LOCK_AUTHOR_PUBKY = 'qr3xqyz3e5cyf9npgxc5zfp15ehhcis6gqsxob4une7bwwazekry';
-const MOCK_LOCK_FILE: LockFile = {
- version: 1,
- creator: 'pubkycreator123',
- primary_resource: {
- path: '/priv/locks.app/content/example.txt',
- hash: '',
- content_type: 'text/plain',
- size: 13,
- },
- secondary_resources: {},
- criteria: [{ criterion_id: 'criterion-1', verifier_type: 'password', params: { satisfied: true } }],
- lock_logic: { type: 'all', criteria: ['criterion-1'] },
- access_policy: { requested_credential_ttl_seconds: 900 },
- lock_server: { override: 'pubkyserver123' },
-};
+const MOCK_LOCK_FILE = mockLockFile();
describe('LockContentParser', () => {
describe('parse', () => {
diff --git a/src/core/pipes/locks/locks.parser.ts b/src/core/pipes/locks/locks.parser.ts
index 84fbf08a69..3e3eda03b7 100644
--- a/src/core/pipes/locks/locks.parser.ts
+++ b/src/core/pipes/locks/locks.parser.ts
@@ -89,8 +89,7 @@ export class LockProofBundler {
private constructor() {}
static build(lockFile: LockFile, lockUrl: string, bundleId: string): TSubmittedProofBundle {
- // TODO:[Locks] #2040 — every criterion is dev-static in Phase 1, so each proof just asserts
- // satisfied; real verifier payloads (password/payment) land with the server verifiers.
+ // TODO:[Locks] #2369 — password and `dev-static` all go away here.
const proofs: TProof[] = lockFile.criteria.map((criterion) => ({
criterion_id: criterion.criterion_id,
verifier_type: criterion.verifier_type,
diff --git a/src/core/services/locks/locks.ts b/src/core/services/locks/locks.ts
index e4b43db3c7..82beb7a6af 100644
--- a/src/core/services/locks/locks.ts
+++ b/src/core/services/locks/locks.ts
@@ -109,7 +109,6 @@ export class LocksService {
/**
* Reads + validates a public lock file via the SDK (pkarr resolve + GET inside `readContentLock`).
- * TODO:[Locks] #2040 — lock-sdk returns `any`; validate this response with Zod instead of casting.
*/
static async readContentLock(lockUrl: string): Promise {
try {
@@ -133,8 +132,7 @@ export class LocksService {
}
// Reader calls are public (no session) → `toAppError`, not `toLocksError` (a 401 isn't an expired session).
- // TODO:[Locks] #2040 — lock-sdk returns `any`; validate this response with Zod instead of casting.
- // TODO:[Locks] #2040 — `password` reaches here but isn't forwarded to lock-sdk (no password verifier yet).
+ // TODO:[Locks] #2369 — password and `dev-static` all go away here.
static async submitProofBundle(bundle: TSubmittedProofBundle, _password: string): Promise {
try {
const viewer = await this.getViewer();
@@ -144,7 +142,6 @@ export class LocksService {
}
}
- // TODO:[Locks] #2040 — lock-sdk returns `any`; validate this response with Zod instead of casting.
static async lookupVerificationTask(creator: string, bundleId: string): Promise {
try {
const viewer = await this.getViewer();
@@ -156,7 +153,6 @@ export class LocksService {
}
}
- // TODO:[Locks] #2040 — lock-sdk returns `any`; validate this response with Zod instead of casting.
static async issueAccessCredential(creator: string, bundleId: string): Promise {
try {
const viewer = await this.getViewer();
@@ -263,7 +259,6 @@ export class LocksService {
const session = getLockSession();
try {
await ensureLocksSdkReady();
- // TODO:[Locks] #2040 — lock-sdk returns `any`; validate this response with Zod instead of casting.
const response = (await session.creator.registerGuardedResource(
new RegisterGuardedResourceOptions(path, contentType, bytes),
)) as { creator: string; guarded_resource: TGuardedResource };
@@ -312,7 +307,8 @@ export class LocksService {
.lockServer({ override: session.lockServer() })
.build();
- // TODO:[Locks] #2040 — lock-sdk returns `any`; validate this response with Zod instead of casting.
+ // TODO:[Locks] the SDK hands creator responses to JS unparsed (locks#22). Viewer
+ // responses are parsed on the Rust side, so only the creator ones need a guard until that lands.
const response = (await session.creator.createContentLock(body)) as {
lock_id: string;
content_lock_path: string;
diff --git a/src/core/services/locks/locks.types.ts b/src/core/services/locks/locks.types.ts
index ef097ff7c7..4ef2f6e12f 100644
--- a/src/core/services/locks/locks.types.ts
+++ b/src/core/services/locks/locks.types.ts
@@ -46,13 +46,7 @@ export type TGuardedResource = {
size: number;
};
-/**
- * One unlock criterion.
- *
- * TODO:[Locks] #2040 — Phase 1 ships a `password` verifier, but the Lock Server only registers
- * `dev-static` today, so that placeholder is sent instead — see the content methods on
- * `LocksController`. Drop this note once the password verifier lands and the placeholder is gone.
- */
+/** One unlock criterion. */
type TLockCriterion = {
criterion_id: string;
verifier_type: string;
@@ -168,7 +162,7 @@ interface LockServer {
* The Lock server is a standalone service (not pubky.app-specific), so this type
* belongs to the Lock SDK — hand-mirrored here until that ships a typed reader API.
* The password/payment distinction is read from each criterion's `verifier_type`.
- * TODO:[Locks] #1998 — replace with the Lock SDK's type once available.
+ * TODO:[Locks] locks#22 — replace with the SDK's own type once it exports one.
*/
export interface LockFile {
version: number;
diff --git a/src/core/services/locks/locks.utils.ts b/src/core/services/locks/locks.utils.ts
index bba838948c..1c2d972523 100644
--- a/src/core/services/locks/locks.utils.ts
+++ b/src/core/services/locks/locks.utils.ts
@@ -33,7 +33,7 @@ let sdkReady: Promise | null = null;
* build of the SDK (wasm-pack `--target web`) requires this before any SDK class is used —
* unlike `@synonymdev/pubky` / `pubky-app-specs`, which self-initialize on import.
*
- * TODO: This only exists because the SDK is shipped as the web build. If the SDK is published
+ * TODO:[Locks] #2372 — This only exists because the SDK is shipped as the web build. If the SDK is published
* as a bundler or self-contained (base64-inlined) build instead, wasm initializes on import and
* this becomes unnecessary — the app would just `import` the SDK like the other wasm deps. Prefer
* that; ask the SDK maintainers to ship it self-contained. Reference for the self-contained
diff --git a/src/core/services/nexus/nexus.types.ts b/src/core/services/nexus/nexus.types.ts
index 7959d940ac..4d1dea8260 100644
--- a/src/core/services/nexus/nexus.types.ts
+++ b/src/core/services/nexus/nexus.types.ts
@@ -209,12 +209,7 @@ export type NexusPostDetails = {
kind: string;
uri: string;
attachments: string[] | null;
- /**
- * TODO:[Locks] #1998 — top-level URL to the post's public `lock.json`; marks the
- * post as a lock teaser and is the single source for lock detection
- * (`!!postDetails.lock`). Nexus does not return this yet (pubky-app-specs#136) —
- * only the FE mock seeds it today. Remove this note once spec/Nexus deliver it.
- */
+ /** URL of the post's public `lock.json`. Present = lock teaser; the sole lock-detection signal. */
lock?: string | null;
};
diff --git a/src/core/stores/locksAuth/locksAuth.types.ts b/src/core/stores/locksAuth/locksAuth.types.ts
index f4db6805c7..55a593cb29 100644
--- a/src/core/stores/locksAuth/locksAuth.types.ts
+++ b/src/core/stores/locksAuth/locksAuth.types.ts
@@ -11,7 +11,7 @@ export interface LocksAuthState {
session: LocksSdkSession | null;
/** Persisted bearer secret; the live session is rebuilt from it on load. */
locksSessionSecret: string | null;
- // TODO: when locks-sdk exposes the creator pubky on `Session`, store it here as `creatorPubky`
+ // TODO:[Locks] #2283 — when locks-sdk exposes the creator pubky on `Session`, store it here as `creatorPubky`
// (mirroring the homeserver store's `currentUserPubky`). Callers then read it from the store
// instead of extracting `creator` from Lock Server responses (`LocksService`).
hasHydrated: boolean;
diff --git a/src/hooks/useCreateLockContent/useCreateLockContent.ts b/src/hooks/useCreateLockContent/useCreateLockContent.ts
index eef850c6a0..9ef3015334 100644
--- a/src/hooks/useCreateLockContent/useCreateLockContent.ts
+++ b/src/hooks/useCreateLockContent/useCreateLockContent.ts
@@ -87,6 +87,8 @@ export function useCreateLockContent({
// The lock lives on the Lock-Server-authenticated pubky's homeserver, which may differ from the
// pubky.app account. Build the URL from `lock.creator`, stripping its `pubky` prefix to the raw
// z32 host the `pubky://` scheme expects.
+ // TODO:[Locks] run this through `LockContentParser.isValidLockUrl` before publishing. The read
+ // path already does; the publish path does not, and the announcement below cannot be undone.
const lockUrl = `pubky://${stripPubkyPrefix(lock.creator)}${lock.content_lock_path}`;
// TODO:[Locks] #2181 — the lock and its public lock file already exist. If the announcement below
diff --git a/src/hooks/useLockFile/useLockFile.test.ts b/src/hooks/useLockFile/useLockFile.test.ts
index 0fa15f2931..f8ae5d17ba 100644
--- a/src/hooks/useLockFile/useLockFile.test.ts
+++ b/src/hooks/useLockFile/useLockFile.test.ts
@@ -1,27 +1,11 @@
import { renderHook, waitFor } from '@testing-library/react';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import { LocksController } from '@/controllers/locks/locks';
-import { type LockFile, VerifierType } from '@/services/locks/locks.types';
+import { VerifierType } from '@/services/locks/locks.types';
+import { MOCK_LOCK_AUTHOR_PUBKY, mockLockFile } from '@/test-utils/locks';
import { useLockFile } from './useLockFile';
-// TODO:[Locks] #1998 — inline test fixtures (sample lock file + author pubky) are
-// duplicated across the lock tests; consider extracting a shared test util/fixture.
-const MOCK_LOCK_AUTHOR_PUBKY = 'qr3xqyz3e5cyf9npgxc5zfp15ehhcis6gqsxob4une7bwwazekry';
-const MOCK_LOCK_FILE: LockFile = {
- version: 1,
- creator: 'pubkycreator123',
- primary_resource: {
- path: '/priv/locks.app/content/example.txt',
- hash: '',
- content_type: 'text/plain',
- size: 13,
- },
- secondary_resources: {},
- criteria: [{ criterion_id: 'criterion-1', verifier_type: 'password', params: { satisfied: true } }],
- lock_logic: { type: 'all', criteria: ['criterion-1'] },
- access_policy: { requested_credential_ttl_seconds: 900 },
- lock_server: { override: 'pubkyserver123' },
-};
+const MOCK_LOCK_FILE = mockLockFile();
vi.mock('@/controllers/locks/locks', () => ({
LocksController: { fetchLockFile: vi.fn() },
diff --git a/src/hooks/useLockFile/useLockFile.ts b/src/hooks/useLockFile/useLockFile.ts
index d8cd6545c2..8dd2518468 100644
--- a/src/hooks/useLockFile/useLockFile.ts
+++ b/src/hooks/useLockFile/useLockFile.ts
@@ -12,7 +12,7 @@ import type { UseLockFileResult } from './useLockFile.types';
* (no local cache to read). Failures never block the user —
* the fetch is caught and surfaced as `hasError` for an "unavailable" UI.
*
- * TODO:[Locks] #1998 — persist lock files to idb and read local-first (`useLocalFirstQuery`, ADR-0011).
+ * TODO:[Locks] #2296 — persist lock files to idb and read local-first (`useLocalFirstQuery`, ADR-0011).
* A lock file is effectively immutable (its `lock_id` is a content hash), so a cached copy never goes
* stale. Today every mount re-requests it through the SDK — the homeserver's ETag keeps that a cheap
* 304 (no body re-download), but idb would skip the request (and the SDK call) entirely.
diff --git a/src/hooks/usePostInputLock/usePostInputLock.ts b/src/hooks/usePostInputLock/usePostInputLock.ts
index 6af32db19a..b3203d4314 100644
--- a/src/hooks/usePostInputLock/usePostInputLock.ts
+++ b/src/hooks/usePostInputLock/usePostInputLock.ts
@@ -164,8 +164,7 @@ export function usePostInputLock({
revertToNormalPost();
};
- // TODO:[Locks] #2040 — `password` is dropped: the Lock Server has no password verifier yet, so the
- // lock is created with the `dev-static` placeholder (see `LocksController`).
+ // TODO:[Locks] #2369 — password and `dev-static` all go away here.
const handleLockApplied = (_password: string) => {
setIsLockDialogOpen(false);
setIsLockConfigured(true);
diff --git a/src/hooks/useUnlockedContent/useUnlockedContent.ts b/src/hooks/useUnlockedContent/useUnlockedContent.ts
index ff5521ac04..6b5250c97f 100644
--- a/src/hooks/useUnlockedContent/useUnlockedContent.ts
+++ b/src/hooks/useUnlockedContent/useUnlockedContent.ts
@@ -55,8 +55,8 @@ export function useUnlockedContent({ lock, lockFile, authorId }: UseUnlockedCont
useEffect(() => {
if (!lock || !currentUserPubky || !session) return;
// My own post can't have a replicated copy (unlocking only happens on other people's posts).
- // Leans on the a == b policy: post author == lock creator. TODO:[Locks] #1998 — phase 2 (a != b)
- // breaks that inference; decide by lock ownership (e.g. a local unlock index), not authorship.
+ // Leans on the a == b policy: post author == lock creator. TODO:[Locks] #2283 — a != b breaks
+ // that inference; decide by lock ownership (e.g. a local unlock index), not authorship.
if (authorId === currentUserPubky) return;
let cancelled = false;
@@ -79,7 +79,8 @@ export function useUnlockedContent({ lock, lockFile, authorId }: UseUnlockedCont
if (!isOwnLock) {
// a != b: I posted this but locked it with a different account, so the guarded original lives on
// that account's homeserver and can't be read with this session. Leave it locked.
- // TODO:[Locks] #1998 — phase 2: resolve by forcing the lock-auth account == the pubky.app account.
+ // TODO:[Locks] #2283 — the resolution is still open; forcing the two accounts to match is not
+ // it, since #2001 deliberately allowed them to differ.
if (authorId === currentUserPubky) {
Logger.warn('[Locks] own lock posted under a different account — guarded original unreadable (phase 2)', {
lock,
diff --git a/src/test-utils/README.md b/src/test-utils/README.md
index 19b1d5e1dd..5e9ca930c3 100644
--- a/src/test-utils/README.md
+++ b/src/test-utils/README.md
@@ -13,6 +13,7 @@ Rule of thumb when you reach for a cast in a test:
| Building a partial `@synonymdev/pubky` `Session` or `Keypair` | `mockSession()` / `mockKeypair()` |
| Building a Zustand store double with only a few fields populated | `mockAuthStore()`, `mockOnboardingStore()`, `mockMigrationStore()`, `mockNotificationStore()`, `mockHomeStore()`, `mockSettingsStore()`, `mockSignInStore()`, `mockLocalFilesStore()`, `mockHotStore()`, `mockSearchStore()` |
| Building a partial React synthetic event | `mockDragEvent()`, `mockClipboardEvent()`, `mockKeyboardEvent()`, `mockMouseEvent()`, `mockAnimationEvent()` |
+| Building a sample Locks `lock.json` | `mockLockFile()` |
| Building a partial `fetch` `Response` | `mockResponse()` |
Every helper encapsulates the escape hatch in one place with a named type
diff --git a/src/test-utils/locks.ts b/src/test-utils/locks.ts
new file mode 100644
index 0000000000..779ada1269
--- /dev/null
+++ b/src/test-utils/locks.ts
@@ -0,0 +1,21 @@
+import type { LockFile } from '@/services/locks/locks.types';
+
+export const MOCK_LOCK_AUTHOR_PUBKY = 'qr3xqyz3e5cyf9npgxc5zfp15ehhcis6gqsxob4une7bwwazekry';
+
+/** Sample public `lock.json`. Pass overrides for the one field a test cares about. */
+export const mockLockFile = (overrides: Partial = {}): LockFile => ({
+ version: 1,
+ creator: 'pubkycreator123',
+ primary_resource: {
+ path: '/priv/locks.app/content/example.txt',
+ hash: '',
+ content_type: 'text/plain',
+ size: 13,
+ },
+ secondary_resources: {},
+ criteria: [{ criterion_id: 'criterion-1', verifier_type: 'password', params: { satisfied: true } }],
+ lock_logic: { type: 'all', criteria: ['criterion-1'] },
+ access_policy: { requested_credential_ttl_seconds: 900 },
+ lock_server: { override: 'pubkyserver123' },
+ ...overrides,
+});