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, +});