this repo has no description
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

Delete dead test files from pre-SDK era, fix review findings

Remove 3 test files importing from deleted storageTypes stub — broken
since the SDK migration. Add checkSession to CRATE_STRUCTURE.md WASM
exports. Limit console.warn in doRefresh to message-only (no endpoint
URLs or nonce values in browser console).

+3 -762
-2
apps/web/src/lib/storageTypes.ts
··· 1 - // Stubbed — replaced by @opake/sdk 2 - throw new Error("unimplemented");
-291
apps/web/tests/lib/indexeddb-cache.test.ts
··· 1 - import "fake-indexeddb/auto"; 2 - import { describe, it, expect, beforeEach, afterEach } from "vitest"; 3 - import { IndexedDbStorage } from "../../src/lib/indexeddbStorage"; 4 - import type { CachedRecord, CachedCollection } from "../../src/lib/storageTypes"; 5 - 6 - let storage: IndexedDbStorage; 7 - let dbCounter = 0; 8 - 9 - function uniqueDbName(): string { 10 - return `opake-cache-test-${++dbCounter}-${Date.now()}`; 11 - } 12 - 13 - const DID = "did:plc:alice"; 14 - const COLLECTION = "app.opake.document"; 15 - 16 - function fakeRecord(rkey: string, name: string): CachedRecord<{ name: string }> { 17 - return { 18 - uri: `at://${DID}/${COLLECTION}/${rkey}`, 19 - cid: `bafycid${rkey}`, 20 - value: { name }, 21 - }; 22 - } 23 - 24 - beforeEach(() => { 25 - storage = new IndexedDbStorage(uniqueDbName()); 26 - }); 27 - 28 - afterEach(async () => { 29 - await storage.destroy(); 30 - }); 31 - 32 - // -- Record-level ------------------------------------------------------------- 33 - 34 - describe("cacheGetRecord / cachePutRecords", () => { 35 - it("returns null for uncached record", async () => { 36 - const result = await storage.cacheGetRecord(DID, COLLECTION, "at://nope"); 37 - expect(result).toBeNull(); 38 - }); 39 - 40 - it("put then get roundtrips a single record", async () => { 41 - const record = fakeRecord("r1", "alpha"); 42 - await storage.cachePutRecords(DID, COLLECTION, [record]); 43 - const loaded = await storage.cacheGetRecord(DID, COLLECTION, record.uri); 44 - expect(loaded).toEqual(record); 45 - }); 46 - 47 - it("put multiple records and get each individually", async () => { 48 - const records = [fakeRecord("r1", "alpha"), fakeRecord("r2", "beta")]; 49 - await storage.cachePutRecords(DID, COLLECTION, records); 50 - 51 - const r1 = await storage.cacheGetRecord(DID, COLLECTION, records[0].uri); 52 - const r2 = await storage.cacheGetRecord(DID, COLLECTION, records[1].uri); 53 - expect(r1?.value).toEqual({ name: "alpha" }); 54 - expect(r2?.value).toEqual({ name: "beta" }); 55 - }); 56 - 57 - it("upserts existing records (same URI, new CID)", async () => { 58 - const original = fakeRecord("r1", "original"); 59 - await storage.cachePutRecords(DID, COLLECTION, [original]); 60 - 61 - const updated: CachedRecord<{ name: string }> = { 62 - ...original, 63 - cid: "bafynewcid", 64 - value: { name: "updated" }, 65 - }; 66 - await storage.cachePutRecords(DID, COLLECTION, [updated]); 67 - 68 - const loaded = await storage.cacheGetRecord(DID, COLLECTION, original.uri); 69 - expect(loaded?.cid).toBe("bafynewcid"); 70 - expect(loaded?.value).toEqual({ name: "updated" }); 71 - }); 72 - 73 - it("isolates records by DID", async () => { 74 - const aliceRecord = fakeRecord("r1", "alice-doc"); 75 - const bobRecord: CachedRecord<{ name: string }> = { 76 - ...fakeRecord("r1", "bob-doc"), 77 - uri: `at://did:plc:bob/${COLLECTION}/r1`, 78 - }; 79 - 80 - await storage.cachePutRecords(DID, COLLECTION, [aliceRecord]); 81 - await storage.cachePutRecords("did:plc:bob", COLLECTION, [bobRecord]); 82 - 83 - const alice = await storage.cacheGetRecord(DID, COLLECTION, aliceRecord.uri); 84 - const bob = await storage.cacheGetRecord("did:plc:bob", COLLECTION, bobRecord.uri); 85 - expect(alice?.value).toEqual({ name: "alice-doc" }); 86 - expect(bob?.value).toEqual({ name: "bob-doc" }); 87 - }); 88 - 89 - it("isolates records by collection", async () => { 90 - const docRecord = fakeRecord("r1", "a-doc"); 91 - const dirRecord: CachedRecord<{ name: string }> = { 92 - uri: `at://${DID}/app.opake.directory/r1`, 93 - cid: "bafydir", 94 - value: { name: "a-dir" }, 95 - }; 96 - 97 - await storage.cachePutRecords(DID, COLLECTION, [docRecord]); 98 - await storage.cachePutRecords(DID, "app.opake.directory", [dirRecord]); 99 - 100 - const doc = await storage.cacheGetRecord(DID, COLLECTION, docRecord.uri); 101 - const dir = await storage.cacheGetRecord(DID, "app.opake.directory", dirRecord.uri); 102 - expect(doc?.value).toEqual({ name: "a-doc" }); 103 - expect(dir?.value).toEqual({ name: "a-dir" }); 104 - }); 105 - }); 106 - 107 - describe("cacheRemoveRecord", () => { 108 - it("removes a specific record", async () => { 109 - const records = [fakeRecord("r1", "alpha"), fakeRecord("r2", "beta")]; 110 - await storage.cachePutRecords(DID, COLLECTION, records); 111 - 112 - await storage.cacheRemoveRecord(DID, COLLECTION, records[0].uri); 113 - 114 - const removed = await storage.cacheGetRecord(DID, COLLECTION, records[0].uri); 115 - const kept = await storage.cacheGetRecord(DID, COLLECTION, records[1].uri); 116 - expect(removed).toBeNull(); 117 - expect(kept).not.toBeNull(); 118 - }); 119 - 120 - it("no-op for nonexistent record", async () => { 121 - await storage.cacheRemoveRecord(DID, COLLECTION, "at://nope"); 122 - }); 123 - }); 124 - 125 - // -- Collection-level --------------------------------------------------------- 126 - 127 - describe("cacheGetCollection / cachePutCollection", () => { 128 - it("returns null when collection was never fully fetched", async () => { 129 - const result = await storage.cacheGetCollection(DID, COLLECTION); 130 - expect(result).toBeNull(); 131 - }); 132 - 133 - it("returns null even if individual records exist (no fetchedAt)", async () => { 134 - await storage.cachePutRecords(DID, COLLECTION, [fakeRecord("r1", "alpha")]); 135 - const result = await storage.cacheGetCollection(DID, COLLECTION); 136 - expect(result).toBeNull(); 137 - }); 138 - 139 - it("put collection then get roundtrips", async () => { 140 - const data: CachedCollection<{ name: string }> = { 141 - records: [fakeRecord("r1", "alpha"), fakeRecord("r2", "beta")], 142 - fetchedAt: 1700000000000, 143 - }; 144 - await storage.cachePutCollection(DID, COLLECTION, data); 145 - 146 - const loaded = await storage.cacheGetCollection<{ name: string }>(DID, COLLECTION); 147 - expect(loaded).not.toBeNull(); 148 - expect(loaded!.fetchedAt).toBe(1700000000000); 149 - expect(loaded!.records).toHaveLength(2); 150 - expect(loaded!.records.map((r) => r.value.name).sort()).toEqual(["alpha", "beta"]); 151 - }); 152 - 153 - it("replaces previous collection atomically", async () => { 154 - const v1: CachedCollection<{ name: string }> = { 155 - records: [fakeRecord("r1", "old-a"), fakeRecord("r2", "old-b")], 156 - fetchedAt: 1000, 157 - }; 158 - await storage.cachePutCollection(DID, COLLECTION, v1); 159 - 160 - const v2: CachedCollection<{ name: string }> = { 161 - records: [fakeRecord("r3", "new-c")], 162 - fetchedAt: 2000, 163 - }; 164 - await storage.cachePutCollection(DID, COLLECTION, v2); 165 - 166 - const loaded = await storage.cacheGetCollection<{ name: string }>(DID, COLLECTION); 167 - expect(loaded!.fetchedAt).toBe(2000); 168 - expect(loaded!.records).toHaveLength(1); 169 - expect(loaded!.records[0].value.name).toBe("new-c"); 170 - 171 - // Old records should be gone 172 - const oldR1 = await storage.cacheGetRecord(DID, COLLECTION, `at://${DID}/${COLLECTION}/r1`); 173 - expect(oldR1).toBeNull(); 174 - }); 175 - 176 - it("individual put records are visible in get collection after putCollection sets fetchedAt", async () => { 177 - await storage.cachePutCollection(DID, COLLECTION, { 178 - records: [fakeRecord("r1", "alpha")], 179 - fetchedAt: 1000, 180 - }); 181 - 182 - // Add a record individually (simulating ensureDirectoryReady cache write) 183 - await storage.cachePutRecords(DID, COLLECTION, [fakeRecord("r2", "beta")]); 184 - 185 - const loaded = await storage.cacheGetCollection<{ name: string }>(DID, COLLECTION); 186 - expect(loaded!.records).toHaveLength(2); 187 - }); 188 - }); 189 - 190 - describe("cacheInvalidateCollection", () => { 191 - it("clears fetchedAt so getCollection returns null", async () => { 192 - await storage.cachePutCollection(DID, COLLECTION, { 193 - records: [fakeRecord("r1", "alpha")], 194 - fetchedAt: 1000, 195 - }); 196 - 197 - await storage.cacheInvalidateCollection(DID, COLLECTION); 198 - 199 - const collection = await storage.cacheGetCollection(DID, COLLECTION); 200 - expect(collection).toBeNull(); 201 - }); 202 - 203 - it("preserves individual records after invalidation", async () => { 204 - await storage.cachePutCollection(DID, COLLECTION, { 205 - records: [fakeRecord("r1", "alpha")], 206 - fetchedAt: 1000, 207 - }); 208 - 209 - await storage.cacheInvalidateCollection(DID, COLLECTION); 210 - 211 - const record = await storage.cacheGetRecord(DID, COLLECTION, `at://${DID}/${COLLECTION}/r1`); 212 - expect(record).not.toBeNull(); 213 - expect(record!.value).toEqual({ name: "alpha" }); 214 - }); 215 - }); 216 - 217 - // -- Account-level ------------------------------------------------------------ 218 - 219 - describe("cacheClear", () => { 220 - it("removes all cache data for an account", async () => { 221 - await storage.cachePutCollection(DID, COLLECTION, { 222 - records: [fakeRecord("r1", "alpha")], 223 - fetchedAt: 1000, 224 - }); 225 - await storage.cachePutCollection(DID, "app.opake.directory", { 226 - records: [{ uri: `at://${DID}/app.opake.directory/d1`, cid: "bafydir", value: { name: "dir" } }], 227 - fetchedAt: 1000, 228 - }); 229 - 230 - await storage.cacheClear(DID); 231 - 232 - expect(await storage.cacheGetCollection(DID, COLLECTION)).toBeNull(); 233 - expect(await storage.cacheGetCollection(DID, "app.opake.directory")).toBeNull(); 234 - expect(await storage.cacheGetRecord(DID, COLLECTION, `at://${DID}/${COLLECTION}/r1`)).toBeNull(); 235 - }); 236 - 237 - it("does not affect other accounts", async () => { 238 - await storage.cachePutCollection(DID, COLLECTION, { 239 - records: [fakeRecord("r1", "alice-doc")], 240 - fetchedAt: 1000, 241 - }); 242 - 243 - const bobUri = `at://did:plc:bob/${COLLECTION}/r1`; 244 - await storage.cachePutCollection("did:plc:bob", COLLECTION, { 245 - records: [{ uri: bobUri, cid: "bafybob", value: { name: "bob-doc" } }], 246 - fetchedAt: 1000, 247 - }); 248 - 249 - await storage.cacheClear(DID); 250 - 251 - expect(await storage.cacheGetRecord(DID, COLLECTION, `at://${DID}/${COLLECTION}/r1`)).toBeNull(); 252 - const bobRecord = await storage.cacheGetRecord("did:plc:bob", COLLECTION, bobUri); 253 - expect(bobRecord).not.toBeNull(); 254 - expect(bobRecord!.value).toEqual({ name: "bob-doc" }); 255 - }); 256 - }); 257 - 258 - // -- removeAccount clears cache ----------------------------------------------- 259 - 260 - describe("removeAccount clears cache", () => { 261 - it("removes cached records alongside identity/session/config", async () => { 262 - const config = { 263 - defaultDid: DID, 264 - accounts: { [DID]: { pdsUrl: "https://pds.test", handle: "alice.test" } }, 265 - }; 266 - await storage.saveConfig(config); 267 - await storage.saveIdentity(DID, { 268 - did: DID, 269 - public_key: "AAAA", 270 - private_key: "BBBB", 271 - signing_key: null, 272 - verify_key: null, 273 - }); 274 - await storage.saveSession(DID, { 275 - type: "legacy", 276 - did: DID, 277 - handle: "alice.test", 278 - accessJwt: "jwt", 279 - refreshJwt: "jwt", 280 - }); 281 - await storage.cachePutCollection(DID, COLLECTION, { 282 - records: [fakeRecord("r1", "alpha")], 283 - fetchedAt: 1000, 284 - }); 285 - 286 - await storage.removeAccount(DID); 287 - 288 - expect(await storage.cacheGetCollection(DID, COLLECTION)).toBeNull(); 289 - expect(await storage.cacheGetRecord(DID, COLLECTION, `at://${DID}/${COLLECTION}/r1`)).toBeNull(); 290 - }); 291 - });
-232
apps/web/tests/lib/indexeddb-storage.test.ts
··· 1 - import "fake-indexeddb/auto"; 2 - import { describe, it, expect, beforeEach, afterEach } from "vitest"; 3 - import { IndexedDbStorage } from "../../src/lib/indexeddbStorage"; 4 - import { StorageError } from "../../src/lib/storage"; 5 - import type { 6 - AccountEntry, 7 - Config, 8 - Identity, 9 - Session, 10 - } from "../../src/lib/storageTypes"; 11 - 12 - let storage: IndexedDbStorage; 13 - let dbCounter = 0; 14 - 15 - function uniqueDbName(): string { 16 - return `opake-test-${++dbCounter}-${Date.now()}`; 17 - } 18 - 19 - const testConfig: Config = { 20 - defaultDid: "did:plc:alice", 21 - accounts: { 22 - "did:plc:alice": { pdsUrl: "https://pds.alice", handle: "alice.test" }, 23 - }, 24 - }; 25 - 26 - const testIdentity: Identity = { 27 - did: "did:plc:alice", 28 - public_key: "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=", 29 - private_key: "AQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQE=", 30 - signing_key: "AgICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgI=", 31 - verify_key: "AwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwMDAwM=", 32 - }; 33 - 34 - const testSession: Session = { 35 - type: "legacy", 36 - did: "did:plc:alice", 37 - handle: "alice.test", 38 - accessJwt: "eyJ.access.token", 39 - refreshJwt: "eyJ.refresh.token", 40 - }; 41 - 42 - beforeEach(() => { 43 - storage = new IndexedDbStorage(uniqueDbName()); 44 - }); 45 - 46 - afterEach(async () => { 47 - await storage.destroy(); 48 - }); 49 - 50 - // -- Config ------------------------------------------------------------------- 51 - 52 - describe("config", () => { 53 - it("save and load roundtrip", async () => { 54 - await storage.saveConfig(testConfig); 55 - const loaded = await storage.loadConfig(); 56 - expect(loaded).toEqual(testConfig); 57 - }); 58 - 59 - it("load without save throws StorageError", async () => { 60 - await expect(storage.loadConfig()).rejects.toThrow(StorageError); 61 - }); 62 - 63 - it("save overwrites previous config", async () => { 64 - await storage.saveConfig(testConfig); 65 - const updated: Config = { ...testConfig, defaultDid: "did:plc:bob" }; 66 - await storage.saveConfig(updated); 67 - const loaded = await storage.loadConfig(); 68 - expect(loaded.defaultDid).toBe("did:plc:bob"); 69 - }); 70 - 71 - it("handles multiple accounts", async () => { 72 - const config: Config = { 73 - defaultDid: "did:plc:alice", 74 - accounts: { 75 - "did:plc:alice": { pdsUrl: "https://pds.alice", handle: "alice.test" }, 76 - "did:plc:bob": { pdsUrl: "https://pds.bob", handle: "bob.test" }, 77 - }, 78 - }; 79 - await storage.saveConfig(config); 80 - const loaded = await storage.loadConfig(); 81 - expect(Object.keys(loaded.accounts)).toHaveLength(2); 82 - expect(loaded.accounts["did:plc:bob"]?.handle).toBe("bob.test"); 83 - }); 84 - 85 - it("AccountEntry fields roundtrip through config", async () => { 86 - const entry: AccountEntry = { 87 - pdsUrl: "https://pds.carol", 88 - handle: "carol.test", 89 - }; 90 - const config: Config = { 91 - defaultDid: "did:plc:carol", 92 - accounts: { "did:plc:carol": entry }, 93 - }; 94 - await storage.saveConfig(config); 95 - const loaded = await storage.loadConfig(); 96 - const loadedEntry = loaded.accounts["did:plc:carol"]; 97 - expect(loadedEntry).toBeDefined(); 98 - expect(loadedEntry?.pdsUrl).toBe("https://pds.carol"); 99 - expect(loadedEntry?.handle).toBe("carol.test"); 100 - }); 101 - }); 102 - 103 - // -- Identity ----------------------------------------------------------------- 104 - 105 - describe("identity", () => { 106 - it("save and load roundtrip", async () => { 107 - await storage.saveIdentity("did:plc:alice", testIdentity); 108 - const loaded = await storage.loadIdentity("did:plc:alice"); 109 - expect(loaded).toEqual(testIdentity); 110 - }); 111 - 112 - it("load missing identity throws StorageError", async () => { 113 - await expect(storage.loadIdentity("did:plc:nobody")).rejects.toThrow( 114 - StorageError, 115 - ); 116 - }); 117 - 118 - it("overwrite preserves latest", async () => { 119 - await storage.saveIdentity("did:plc:alice", testIdentity); 120 - const updated: Identity = { ...testIdentity, public_key: "NEWKEY=" }; 121 - await storage.saveIdentity("did:plc:alice", updated); 122 - const loaded = await storage.loadIdentity("did:plc:alice"); 123 - expect(loaded.public_key).toBe("NEWKEY="); 124 - }); 125 - 126 - it("different DIDs are independent", async () => { 127 - const bobIdentity: Identity = { ...testIdentity, did: "did:plc:bob" }; 128 - await storage.saveIdentity("did:plc:alice", testIdentity); 129 - await storage.saveIdentity("did:plc:bob", bobIdentity); 130 - const alice = await storage.loadIdentity("did:plc:alice"); 131 - const bob = await storage.loadIdentity("did:plc:bob"); 132 - expect(alice.did).toBe("did:plc:alice"); 133 - expect(bob.did).toBe("did:plc:bob"); 134 - }); 135 - }); 136 - 137 - // -- Session ------------------------------------------------------------------ 138 - 139 - describe("session", () => { 140 - it("save and load roundtrip", async () => { 141 - await storage.saveSession("did:plc:alice", testSession); 142 - const loaded = await storage.loadSession("did:plc:alice"); 143 - expect(loaded).toEqual(testSession); 144 - }); 145 - 146 - it("load missing session throws StorageError", async () => { 147 - await expect(storage.loadSession("did:plc:nobody")).rejects.toThrow( 148 - StorageError, 149 - ); 150 - }); 151 - 152 - it("overwrite preserves latest", async () => { 153 - await storage.saveSession("did:plc:alice", testSession); 154 - const refreshed: Session = { ...testSession, accessJwt: "new.jwt" }; 155 - await storage.saveSession("did:plc:alice", refreshed); 156 - const loaded = await storage.loadSession("did:plc:alice"); 157 - expect(loaded.type).toBe("legacy"); 158 - if (loaded.type === "legacy") { 159 - expect(loaded.accessJwt).toBe("new.jwt"); 160 - } 161 - }); 162 - }); 163 - 164 - // -- removeAccount ------------------------------------------------------------ 165 - 166 - const multiAccountConfig: Config = { 167 - defaultDid: "did:plc:alice", 168 - accounts: { 169 - "did:plc:alice": { pdsUrl: "https://pds.alice", handle: "alice.test" }, 170 - "did:plc:bob": { pdsUrl: "https://pds.bob", handle: "bob.test" }, 171 - }, 172 - }; 173 - 174 - describe("removeAccount", () => { 175 - it("removes identity, session, and config entry", async () => { 176 - await storage.saveConfig(multiAccountConfig); 177 - await storage.saveIdentity("did:plc:alice", testIdentity); 178 - await storage.saveSession("did:plc:alice", testSession); 179 - 180 - await storage.removeAccount("did:plc:alice"); 181 - 182 - await expect(storage.loadIdentity("did:plc:alice")).rejects.toThrow( 183 - StorageError, 184 - ); 185 - await expect(storage.loadSession("did:plc:alice")).rejects.toThrow( 186 - StorageError, 187 - ); 188 - const config = await storage.loadConfig(); 189 - expect(config.accounts["did:plc:alice"]).toBeUndefined(); 190 - }); 191 - 192 - it("promotes next default when removing the current default", async () => { 193 - await storage.saveConfig(multiAccountConfig); 194 - await storage.saveIdentity("did:plc:alice", testIdentity); 195 - await storage.saveSession("did:plc:alice", testSession); 196 - 197 - await storage.removeAccount("did:plc:alice"); 198 - 199 - const config = await storage.loadConfig(); 200 - expect(config.defaultDid).toBe("did:plc:bob"); 201 - expect(Object.keys(config.accounts)).toHaveLength(1); 202 - }); 203 - 204 - it("clears default when removing the last account", async () => { 205 - await storage.saveConfig(testConfig); 206 - await storage.saveIdentity("did:plc:alice", testIdentity); 207 - await storage.saveSession("did:plc:alice", testSession); 208 - 209 - await storage.removeAccount("did:plc:alice"); 210 - 211 - const config = await storage.loadConfig(); 212 - expect(config.defaultDid).toBeNull(); 213 - expect(Object.keys(config.accounts)).toHaveLength(0); 214 - }); 215 - 216 - it("does not affect other accounts", async () => { 217 - await storage.saveConfig(multiAccountConfig); 218 - const bobIdentity: Identity = { ...testIdentity, did: "did:plc:bob" }; 219 - const bobSession: Session = { ...testSession, did: "did:plc:bob" }; 220 - await storage.saveIdentity("did:plc:alice", testIdentity); 221 - await storage.saveSession("did:plc:alice", testSession); 222 - await storage.saveIdentity("did:plc:bob", bobIdentity); 223 - await storage.saveSession("did:plc:bob", bobSession); 224 - 225 - await storage.removeAccount("did:plc:alice"); 226 - 227 - const bob = await storage.loadIdentity("did:plc:bob"); 228 - expect(bob.did).toBe("did:plc:bob"); 229 - const bobSess = await storage.loadSession("did:plc:bob"); 230 - expect(bobSess.did).toBe("did:plc:bob"); 231 - }); 232 - });
-235
apps/web/tests/stores/auth-seed-phrase.test.ts
··· 1 - // Tests for seed phrase store actions — verifies they use the authenticated 2 - // API path (with token refresh) rather than raw fetch with stale tokens. 3 - 4 - import "fake-indexeddb/auto"; 5 - import { describe, it, expect, beforeEach, afterEach, vi } from "vitest"; 6 - import type { OAuthSession, Identity } from "../../src/lib/storageTypes"; 7 - 8 - const TEST_DID = "did:plc:test123"; 9 - const TEST_PDS = "https://pds.test"; 10 - 11 - const testIdentity: Identity = { 12 - did: TEST_DID, 13 - public_key: "dGVzdC1wdWJsaWMta2V5", 14 - private_key: "dGVzdC1wcml2YXRlLWtleQ==", 15 - signing_key: "dGVzdC1zaWduaW5nLWtleQ==", 16 - verify_key: "dGVzdC12ZXJpZnkta2V5", 17 - }; 18 - 19 - const testSession: OAuthSession = { 20 - type: "oauth", 21 - did: TEST_DID, 22 - handle: "test.handle", 23 - accessToken: "test-access-token", 24 - refreshToken: "test-refresh-token", 25 - dpopKey: { privateKeyJwk: "{}", publicKeyJwk: "{}", thumbprint: "thumb" } as never, 26 - tokenEndpoint: "https://auth.test/token", 27 - dpopNonce: "nonce-1", 28 - expiresAt: Math.floor(Date.now() / 1000) + 3600, 29 - clientId: "https://opake.app/client-metadata.json", 30 - }; 31 - 32 - const savedIdentities: Record<string, Identity> = {}; 33 - 34 - // Mock storage singleton used by auth store and api.ts 35 - const mockStorage = { 36 - loadConfig: vi.fn().mockResolvedValue({ 37 - defaultDid: TEST_DID, 38 - accounts: { [TEST_DID]: { pdsUrl: TEST_PDS, handle: "test.handle" } }, 39 - }), 40 - saveConfig: vi.fn().mockResolvedValue(undefined), 41 - loadSession: vi.fn().mockResolvedValue(testSession), 42 - saveSession: vi.fn().mockResolvedValue(undefined), 43 - loadIdentity: vi.fn().mockRejectedValue(new Error("not found")), 44 - saveIdentity: vi.fn().mockImplementation((_did: string, id: Identity) => { 45 - savedIdentities[_did] = id; 46 - return Promise.resolve(); 47 - }), 48 - removeAccount: vi.fn().mockResolvedValue(undefined), 49 - loadProfile: vi.fn().mockResolvedValue(null), 50 - saveProfile: vi.fn().mockResolvedValue(undefined), 51 - }; 52 - 53 - class MockIndexedDbStorage { 54 - loadConfig = mockStorage.loadConfig; 55 - saveConfig = mockStorage.saveConfig; 56 - loadSession = mockStorage.loadSession; 57 - saveSession = mockStorage.saveSession; 58 - loadIdentity = mockStorage.loadIdentity; 59 - saveIdentity = mockStorage.saveIdentity; 60 - removeAccount = mockStorage.removeAccount; 61 - loadProfile = mockStorage.loadProfile; 62 - saveProfile = mockStorage.saveProfile; 63 - } 64 - 65 - vi.mock("../../src/lib/indexeddbStorage", () => ({ 66 - IndexedDbStorage: MockIndexedDbStorage, 67 - storage: new MockIndexedDbStorage(), 68 - })); 69 - 70 - // Mock opake worker 71 - vi.mock("../../src/lib/worker", () => ({ 72 - getOpakeWorker: () => ({ 73 - generateMnemonic: () => "abandon ".repeat(23).trim() + " art", 74 - validateMnemonic: () => true, 75 - deriveIdentityFromMnemonic: (_phrase: string, did: string) => ({ 76 - ...testIdentity, 77 - did, 78 - }), 79 - generateIdentity: (did: string) => ({ ...testIdentity, did }), 80 - createDpopProof: () => "mock-dpop-proof", 81 - }), 82 - })); 83 - 84 - // Mock loading helper 85 - vi.mock("../../src/stores/app", () => ({ 86 - loading: () => () => {}, 87 - })); 88 - 89 - // Import after mocks are set up 90 - const { useAuthStore } = await import("../../src/stores/auth"); 91 - 92 - describe("auth store seed phrase actions (authenticated API)", () => { 93 - // eslint-disable-next-line functional/no-let -- test setup 94 - let fetchSpy: ReturnType<typeof vi.spyOn>; 95 - 96 - beforeEach(() => { 97 - useAuthStore.setState({ 98 - session: { 99 - status: "active", 100 - did: TEST_DID, 101 - handle: "test.handle", 102 - pdsUrl: TEST_PDS, 103 - avatarUrl: null, 104 - bannerUrl: null, 105 - }, 106 - identity: { status: "fresh" }, 107 - }); 108 - 109 - fetchSpy = vi.spyOn(globalThis, "fetch"); 110 - }); 111 - 112 - afterEach(() => { 113 - vi.restoreAllMocks(); 114 - }); 115 - 116 - it("confirmSeedPhrase calls putRecord endpoint (not raw publishPublicKey)", async () => { 117 - fetchSpy.mockResolvedValue( 118 - new Response(JSON.stringify({ uri: "at://test", cid: "cid" }), { 119 - status: 200, 120 - headers: { "content-type": "application/json" }, 121 - }), 122 - ); 123 - 124 - await useAuthStore.getState().confirmSeedPhrase("abandon ".repeat(23).trim() + " art"); 125 - 126 - const putRecordCalls = fetchSpy.mock.calls.filter( 127 - ([url]) => typeof url === "string" && url.includes("putRecord"), 128 - ); 129 - expect(putRecordCalls.length).toBeGreaterThan(0); 130 - 131 - const body = putRecordCalls[0][1]?.body as string; 132 - const parsed = JSON.parse(body) as Record<string, unknown>; 133 - expect(parsed.collection).toBe("app.opake.publicKey"); 134 - expect(parsed.rkey).toBe("self"); 135 - }); 136 - 137 - it("confirmSeedPhrase sets identity to ready on success", async () => { 138 - fetchSpy.mockResolvedValue( 139 - new Response(JSON.stringify({ uri: "at://test", cid: "cid" }), { 140 - status: 200, 141 - headers: { "content-type": "application/json" }, 142 - }), 143 - ); 144 - 145 - await useAuthStore.getState().confirmSeedPhrase("abandon ".repeat(23).trim() + " art"); 146 - expect(useAuthStore.getState().identity.status).toBe("ready"); 147 - }); 148 - 149 - it("confirmSeedPhrase saves identity to storage", async () => { 150 - fetchSpy.mockResolvedValue( 151 - new Response(JSON.stringify({ uri: "at://test", cid: "cid" }), { 152 - status: 200, 153 - headers: { "content-type": "application/json" }, 154 - }), 155 - ); 156 - 157 - await useAuthStore.getState().confirmSeedPhrase("abandon ".repeat(23).trim() + " art"); 158 - expect(savedIdentities[TEST_DID]).toBeDefined(); 159 - expect(savedIdentities[TEST_DID].public_key).toBe(testIdentity.public_key); 160 - }); 161 - 162 - it("confirmSeedPhrase throws on 401 and sets identity to fresh", async () => { 163 - fetchSpy.mockResolvedValue(new Response("Unauthorized", { status: 401 })); 164 - 165 - await expect( 166 - useAuthStore.getState().confirmSeedPhrase("abandon ".repeat(23).trim() + " art"), 167 - ).rejects.toThrow(); 168 - 169 - expect(useAuthStore.getState().identity.status).toBe("fresh"); 170 - }); 171 - 172 - it("recoverFromSeedPhrase calls putRecord when forced", async () => { 173 - // getRecord (upstream key check) → different key 174 - fetchSpy.mockResolvedValueOnce( 175 - new Response( 176 - JSON.stringify({ 177 - uri: "at://test", 178 - cid: "cid", 179 - value: { 180 - publicKey: { $bytes: "ZGlmZmVyZW50LWtleQ==" }, 181 - algo: "x25519", 182 - opakeVersion: 1, 183 - createdAt: new Date().toISOString(), 184 - }, 185 - }), 186 - { status: 200, headers: { "content-type": "application/json" } }, 187 - ), 188 - ); 189 - // putRecord → success 190 - fetchSpy.mockResolvedValueOnce( 191 - new Response(JSON.stringify({ uri: "at://test", cid: "cid" }), { 192 - status: 200, 193 - headers: { "content-type": "application/json" }, 194 - }), 195 - ); 196 - 197 - const result = await useAuthStore 198 - .getState() 199 - .recoverFromSeedPhrase("abandon ".repeat(23).trim() + " art", true); 200 - 201 - expect(result.mismatch).toBe(false); 202 - expect(useAuthStore.getState().identity.status).toBe("ready"); 203 - }); 204 - 205 - it("recoverFromSeedPhrase detects mismatch without forcing", async () => { 206 - // getRecord → different key 207 - fetchSpy.mockResolvedValueOnce( 208 - new Response( 209 - JSON.stringify({ 210 - uri: "at://test", 211 - cid: "cid", 212 - value: { 213 - publicKey: { $bytes: "ZGlmZmVyZW50LWtleQ==" }, 214 - algo: "x25519", 215 - opakeVersion: 1, 216 - createdAt: new Date().toISOString(), 217 - }, 218 - }), 219 - { status: 200, headers: { "content-type": "application/json" } }, 220 - ), 221 - ); 222 - 223 - const result = await useAuthStore 224 - .getState() 225 - .recoverFromSeedPhrase("abandon ".repeat(23).trim() + " art"); 226 - 227 - expect(result.mismatch).toBe(true); 228 - 229 - // Should NOT have called putRecord 230 - const putRecordCalls = fetchSpy.mock.calls.filter( 231 - ([url]) => typeof url === "string" && url.includes("putRecord"), 232 - ); 233 - expect(putRecordCalls.length).toBe(0); 234 - }); 235 - });
+1 -1
docs/CRATE_STRUCTURE.md
··· 111 111 src/ 112 112 lib.rs Module declarations, WASM init, pure crypto + tree exports (stateless) 113 113 auth_wasm.rs OAuth login WASM exports: startOAuthLogin, completeOAuthLogin, loginWithAppPasswordWasm. All token handling in WASM. 114 - opake_wasm.rs OpakeContext + WasmFileManagerHandle (owns Opake+FileContext, temporary FileManager borrows per JS call). Also: tokenExpiresAt, proactiveRefresh 114 + opake_wasm.rs OpakeContext + WasmFileManagerHandle (owns Opake+FileContext, temporary FileManager borrows per JS call). Also: tokenExpiresAt, proactiveRefresh, checkSession 115 115 daemon.rs Service Worker maintenance task exports (session refresh, pair cleanup) 116 116 wasm_util.rs make_client, make_opake, make_cabinet, make_workspace helpers. WasmOpake = Opake<WasmTransport, OsRng, NoopStorage> 117 117
+2 -1
packages/opake-sdk/src/opake.ts
··· 432 432 try { 433 433 await ctx.proactiveRefresh(); 434 434 } catch (e) { 435 - console.warn("opake: proactive token refresh failed", e); 435 + const msg = e instanceof Error ? e.message : String(e); 436 + console.warn("opake: proactive token refresh failed:", msg); 436 437 } 437 438 } 438 439