handy online tools for AT Protocol boat.kelinci.net
atproto bluesky atcute typescript solidjs

refactor: use `@atcute/identity` family of packages

mary.my.id 71f52567 fa43b007

verified
+3
package.json
··· 12 12 "@atcute/cbor": "^2.1.3", 13 13 "@atcute/client": "^2.0.8", 14 14 "@atcute/crypto": "^2.2.0", 15 + "@atcute/did-plc": "^0.1.0", 16 + "@atcute/identity": "^0.1.1", 17 + "@atcute/identity-resolver": "^0.1.2", 15 18 "@atcute/multibase": "^1.1.2", 16 19 "@badrap/valita": "^0.4.3", 17 20 "@mary/array-fns": "npm:@jsr/mary__array-fns@^0.1.4",
+46
pnpm-lock.yaml
··· 23 23 '@atcute/crypto': 24 24 specifier: ^2.2.0 25 25 version: 2.2.0 26 + '@atcute/did-plc': 27 + specifier: ^0.1.0 28 + version: 0.1.0 29 + '@atcute/identity': 30 + specifier: ^0.1.1 31 + version: 0.1.1 32 + '@atcute/identity-resolver': 33 + specifier: ^0.1.2 34 + version: 0.1.2(@atcute/identity@0.1.1) 26 35 '@atcute/multibase': 27 36 specifier: ^1.1.2 28 37 version: 1.1.2 ··· 115 124 '@atcute/crypto@2.2.0': 116 125 resolution: {integrity: sha512-Q/64Qn1AI8J0ZNy4hPDPpW/3poKf4OWRUxIYceCDI+btEOcIG5YMlhEQeZd6ojnI3oBMXy03sbOktekaBYRK9Q==} 117 126 127 + '@atcute/did-plc@0.1.0': 128 + resolution: {integrity: sha512-ORW9s9etge/icaJEcbAMiZEFWbydljqHngqU0fSR94KZiK2bXRZxMec7ktpO9oXLDKuSeFH9H23i1xlyqCwjjw==} 129 + 130 + '@atcute/identity-resolver@0.1.2': 131 + resolution: {integrity: sha512-fP2VbHD04kVcCdNi/Kszo6jFzqM7Pg3p33oGhfp2zVkwFKaVBlwCaFRWEga/Xvu/IDLwNdASGWnLqoA34SFeSg==} 132 + peerDependencies: 133 + '@atcute/identity': ^0.1.0 134 + 135 + '@atcute/identity@0.1.1': 136 + resolution: {integrity: sha512-TijKOgvvOfp/QoMAqaiKLn+FnQi5XrxsWLVcVnvr5JoKlgF2yppNvVo0y62XEXZbgDuEMSav1v1tEjC4Hn7MzQ==} 137 + 118 138 '@atcute/multibase@1.1.2': 119 139 resolution: {integrity: sha512-KFX+c7a/u2jSNcRw0rLaUHG/XEKf1A1c8XF5soHnsb1JMCShihf/anfZ1kJ4no/IlIp9HEHV3PQRQO2sWL6ASQ==} 120 140 121 141 '@atcute/uint8array@1.0.1': 122 142 resolution: {integrity: sha512-AAnlFKyfDRgb9GNZJbhQ6OuMhbmNPirQyapb8KnmcEhxQZ3+tt+4NcwqekEegY4MpNqSTYeeTdyxq0wGZv1JHg==} 143 + 144 + '@atcute/util-fetch@1.0.0': 145 + resolution: {integrity: sha512-Mjt5Bow3NApiEhgwuXWwvdZ4fBGgsgXju41MeJvAag1nhg2qObgg19FNccpC63QfXrKdXMGUOJOiZkxIw6wILA==} 123 146 124 147 '@atcute/varint@1.0.2': 125 148 resolution: {integrity: sha512-0O31hePzzr4O3NGWHUKKOyta6CGSL+AtN8iir8grGxu9jXyI7DBARlw6PbgKA6uTAvsXdpmRmF8MX+p0TsLnNg==} ··· 1725 1748 '@atcute/uint8array': 1.0.1 1726 1749 '@noble/secp256k1': 2.2.3 1727 1750 1751 + '@atcute/did-plc@0.1.0': 1752 + dependencies: 1753 + '@atcute/cbor': 2.1.3 1754 + '@atcute/cid': 2.1.0 1755 + '@atcute/crypto': 2.2.0 1756 + '@atcute/multibase': 1.1.2 1757 + '@atcute/uint8array': 1.0.1 1758 + '@badrap/valita': 0.4.3 1759 + 1760 + '@atcute/identity-resolver@0.1.2(@atcute/identity@0.1.1)': 1761 + dependencies: 1762 + '@atcute/identity': 0.1.1 1763 + '@atcute/util-fetch': 1.0.0 1764 + '@badrap/valita': 0.4.3 1765 + 1766 + '@atcute/identity@0.1.1': 1767 + dependencies: 1768 + '@badrap/valita': 0.4.3 1769 + 1728 1770 '@atcute/multibase@1.1.2': 1729 1771 dependencies: 1730 1772 '@atcute/uint8array': 1.0.1 1731 1773 1732 1774 '@atcute/uint8array@1.0.1': {} 1775 + 1776 + '@atcute/util-fetch@1.0.0': 1777 + dependencies: 1778 + '@badrap/valita': 0.4.3 1733 1779 1734 1780 '@atcute/varint@1.0.2': {} 1735 1781
+14 -47
src/api/queries/did-doc.ts
··· 1 - import { At } from '@atcute/client/lexicons'; 1 + import type { AtprotoDid, DidDocument } from '@atcute/identity'; 2 + import { 3 + CompositeDidDocumentResolver, 4 + PlcDidDocumentResolver, 5 + WebDidDocumentResolver, 6 + } from '@atcute/identity-resolver'; 2 7 3 - import { didDocument, DidDocument } from '../types/did-doc'; 4 - import { DID_PLC_RE, DID_WEB_RE } from '../utils/strings'; 8 + const didDocumentResolver = new CompositeDidDocumentResolver({ 9 + methods: { 10 + plc: new PlcDidDocumentResolver(), 11 + web: new WebDidDocumentResolver(), 12 + }, 13 + }); 5 14 6 15 export const getDidDocument = async ({ 7 16 did, 8 17 signal, 9 18 }: { 10 - did: At.DID; 19 + did: AtprotoDid; 11 20 signal?: AbortSignal; 12 21 }): Promise<DidDocument> => { 13 - const colon_index = did.indexOf(':', 4); 14 - 15 - const type = did.slice(4, colon_index); 16 - const ident = did.slice(colon_index + 1); 17 - 18 - let rawDoc: any; 19 - 20 - if (type === 'plc') { 21 - if (!DID_PLC_RE.test(did)) { 22 - throw new Error(`invalid did:plc identifier`); 23 - } 24 - 25 - const origin = import.meta.env.VITE_PLC_DIRECTORY_URL; 26 - const response = await fetch(`${origin}/${did}`, { signal }); 27 - 28 - if (response.status === 404) { 29 - throw new Error(`did not found in directory`); 30 - } else if (!response.ok) { 31 - throw new Error(`directory is unreachable`); 32 - } 33 - 34 - const json = await response.json(); 35 - 36 - rawDoc = json; 37 - } else if (type === 'web') { 38 - if (!DID_WEB_RE.test(did)) { 39 - throw new Error(`invalid did:web identifier`); 40 - } 41 - 42 - const response = await fetch(`https://${ident}/.well-known/did.json`, { signal }); 43 - 44 - if (!response.ok) { 45 - throw new Error(`did document is unreachable`); 46 - } 47 - 48 - const json = await response.json(); 49 - 50 - rawDoc = json; 51 - } else { 52 - throw new Error(`unsupported did method`); 53 - } 54 - 55 - return didDocument.parse(rawDoc, { mode: 'passthrough' }); 22 + return didDocumentResolver.resolve(did, { signal }); 56 23 };
+15 -19
src/api/queries/handle.ts
··· 1 - import { simpleFetchHandler, XRPC } from '@atcute/client'; 2 - import { At } from '@atcute/client/lexicons'; 1 + import { type AtprotoDid, type Handle, isHandle } from '@atcute/identity'; 2 + import { XrpcHandleResolver } from '@atcute/identity-resolver'; 3 3 4 - import { appViewRpc } from '~/globals/rpc'; 4 + const handleResolver = new XrpcHandleResolver({ 5 + serviceUrl: import.meta.env.VITE_APPVIEW_URL, 6 + }); 5 7 6 8 export const resolveHandleViaAppView = async ({ 7 9 handle, 8 10 signal, 9 11 }: { 10 - handle: string; 12 + handle: Handle; 11 13 signal?: AbortSignal; 12 - }): Promise<At.DID> => { 13 - const { data } = await appViewRpc.get('com.atproto.identity.resolveHandle', { 14 - signal: signal, 15 - params: { handle: handle }, 16 - }); 14 + }): Promise<AtprotoDid> => { 15 + if (!isHandle(handle)) { 16 + throw new Error(`invalid handle: ${handle}`); 17 + } 17 18 18 - return data.did; 19 + return await handleResolver.resolve(handle, { signal }); 19 20 }; 20 21 21 22 export const resolveHandleViaPds = async ({ ··· 24 25 signal, 25 26 }: { 26 27 service: string; 27 - handle: string; 28 + handle: Handle; 28 29 signal?: AbortSignal; 29 - }): Promise<At.DID> => { 30 - const rpc = new XRPC({ handler: simpleFetchHandler({ service }) }); 30 + }): Promise<AtprotoDid> => { 31 + const resolver = new XrpcHandleResolver({ serviceUrl: service }); 31 32 32 - const { data } = await rpc.get('com.atproto.identity.resolveHandle', { 33 - signal, 34 - params: { handle }, 35 - }); 36 - 37 - return data.did; 33 + return await resolver.resolve(handle, { signal }); 38 34 };
+4 -5
src/api/queries/plc.ts
··· 1 - import { At } from '@atcute/client/lexicons'; 1 + import { defs } from '@atcute/did-plc'; 2 + import { Did } from '@atcute/identity'; 2 3 3 - import { plcLogEntries } from '../types/plc'; 4 - 5 - export const getPlcAuditLogs = async ({ did, signal }: { did: At.DID; signal?: AbortSignal }) => { 4 + export const getPlcAuditLogs = async ({ did, signal }: { did: Did<'plc'>; signal?: AbortSignal }) => { 6 5 const origin = import.meta.env.VITE_PLC_DIRECTORY_URL; 7 6 const response = await fetch(`${origin}/${did}/log/audit`, { signal }); 8 7 if (!response.ok) { ··· 10 9 } 11 10 12 11 const json = await response.json(); 13 - return plcLogEntries.parse(json); 12 + return defs.indexedEntryLog.parse(json); 14 13 };
-86
src/api/types/did-doc.ts
··· 1 - import * as v from '@badrap/valita'; 2 - 3 - import { didString, serviceUrlString, urlString } from './strings'; 4 - 5 - const PUBLIC_KEY_MULTIBASE_RE = /^z[a-km-zA-HJ-NP-Z1-9]+$/; 6 - 7 - const verificationMethod = v.object({ 8 - id: v.string(), 9 - type: v.string(), 10 - controller: didString, 11 - publicKeyMultibase: v 12 - .string() 13 - .assert((input) => PUBLIC_KEY_MULTIBASE_RE.test(input), `must be a valid base58btc multibase key`), 14 - }); 15 - 16 - const service = v 17 - .object({ 18 - id: v.string(), 19 - type: v.string(), 20 - serviceEndpoint: v.union(urlString, v.record(urlString), v.array(urlString)), 21 - }) 22 - .chain((input) => { 23 - switch (input.type) { 24 - case 'AtprotoPersonalDataServer': 25 - case 'AtprotoLabeler': 26 - case 'BskyFeedGenerator': 27 - case 'BskyNotificationService': { 28 - const result = serviceUrlString.try(input.serviceEndpoint); 29 - if (!result.ok) { 30 - return v.err({ 31 - message: `must be a valid atproto service url`, 32 - path: ['serviceEndpoint'], 33 - }); 34 - } 35 - } 36 - } 37 - 38 - return v.ok(input); 39 - }); 40 - 41 - export const didDocument = v.object({ 42 - '@context': v.array(urlString), 43 - id: didString, 44 - alsoKnownAs: v.array(urlString).optional(() => []), 45 - verificationMethod: v.array(verificationMethod).optional(() => []), 46 - service: v.array(service).chain((input) => { 47 - for (let i = 0, len = input.length; i < len; i++) { 48 - const service = input[i]; 49 - const id = service.id; 50 - 51 - for (let j = 0; j < i; j++) { 52 - if (input[j].id === id) { 53 - return v.err({ 54 - message: `duplicate service id`, 55 - path: [i, 'id'], 56 - }); 57 - } 58 - } 59 - } 60 - 61 - return v.ok(input); 62 - }), 63 - }); 64 - 65 - export type DidDocument = v.Infer<typeof didDocument>; 66 - 67 - export const getPdsEndpoint = (doc: DidDocument): string | undefined => { 68 - return getServiceEndpoint(doc, '#atproto_pds', 'AtprotoPersonalDataServer'); 69 - }; 70 - 71 - export const getServiceEndpoint = ( 72 - doc: DidDocument, 73 - serviceId: string, 74 - serviceType: string, 75 - ): string | undefined => { 76 - const did = doc.id; 77 - 78 - const didServiceId = did + serviceId; 79 - const found = doc.service?.find((service) => service.id === serviceId || service.id === didServiceId); 80 - 81 - if (!found || found.type !== serviceType || typeof found.serviceEndpoint !== 'string') { 82 - return undefined; 83 - } 84 - 85 - return found.serviceEndpoint; 86 - };
+26 -78
src/api/types/plc.ts
··· 1 1 import * as v from '@badrap/valita'; 2 2 3 - import { didKeyString, didString, handleString, serviceUrlString, urlString } from './strings'; 4 - 5 - const legacyGenesisOp = v.object({ 6 - type: v.literal('create'), 7 - signingKey: didKeyString, 8 - recoveryKey: didKeyString, 9 - handle: handleString, 10 - service: serviceUrlString, 11 - prev: v.null(), 12 - sig: v.string(), 13 - }); 14 - 15 - const tombstoneOp = v.object({ 16 - type: v.literal('plc_tombstone'), 17 - prev: v.string(), 18 - sig: v.string(), 19 - }); 20 - 21 - const service = v.object({ 22 - type: v.string().assert((input) => input.length <= 256, `service type too long (max 256)`), 23 - endpoint: urlString.assert((input) => input.length <= 512, `service endpoint too long (max 512)`), 24 - }); 25 - export type Service = v.Infer<typeof service>; 26 - 27 - const updateOp = v.object({ 28 - type: v.literal('plc_operation'), 29 - prev: v.string().nullable(), 30 - sig: v.string(), 31 - rotationKeys: v.array(didKeyString).chain((input) => { 32 - const len = input.length; 33 - 34 - if (len === 0) { 35 - return v.err({ message: `missing rotation keys` }); 36 - } else if (len > 10) { 37 - return v.err({ message: `too many rotation keys (max 10)` }); 38 - } 39 - 40 - for (let i = 0; i < len; i++) { 41 - const key = input[i]; 42 - 43 - for (let j = 0; j < i; j++) { 44 - if (input[j] === key) { 45 - return v.err({ 46 - message: `duplicate rotation key`, 47 - path: [i], 48 - }); 49 - } 50 - } 51 - } 52 - 53 - return v.ok(input); 54 - }), 55 - verificationMethods: v.record(didKeyString), 56 - alsoKnownAs: v 57 - .array(urlString.assert((input) => input.length <= 256, `alsoKnownAs entry too long (max 256)`)) 58 - .assert((input) => input.length <= 10, `too many alsoKnownAs entries (max 10)`), 59 - services: v 60 - .record(service) 61 - .assert((input) => Object.keys(input).length <= 10, `too many service entries (max 10)`), 62 - }); 63 - export type PlcUpdateOp = v.Infer<typeof updateOp>; 3 + import { defs, UnsignedOperation } from '@atcute/did-plc'; 64 4 65 - const plcOperation = v.union(legacyGenesisOp, tombstoneOp, updateOp); 66 - 67 - export const plcLogEntry = v.object({ 68 - did: didString, 69 - cid: v.string(), 70 - operation: plcOperation, 71 - nullified: v.boolean(), 72 - createdAt: v 73 - .string() 74 - .assert((input) => !Number.isNaN(new Date(input).getTime()), `must be a valid datetime string`), 75 - }); 76 - export type PlcLogEntry = v.Infer<typeof plcLogEntry>; 5 + import { ToValidator } from '../utils/valita'; 6 + import { serviceUrlString } from './strings'; 77 7 78 - export const plcLogEntries = v.array(plcLogEntry); 8 + const _unsignedOperation = defs.unsignedOperation as ToValidator<UnsignedOperation>; 79 9 80 - export const updatePayload = updateOp.omit('type', 'prev', 'sig').extend({ 10 + export const updatePayload = _unsignedOperation.omit('type', 'prev').extend({ 81 11 services: v 82 12 .record( 83 - service.chain((input) => { 13 + defs.service.chain((input) => { 84 14 switch (input.type) { 85 15 case 'AtprotoPersonalDataServer': 86 16 case 'AtprotoLabeler': ··· 107 37 return v.ok(input); 108 38 }), 109 39 ) 110 - .assert((input) => Object.keys(input).length <= 10, `too many service entries (max 10)`), 40 + .chain((input) => { 41 + const length = Object.keys(input).length; 42 + 43 + if (length > 10) { 44 + return v.err(`too many service entries (max 10)`); 45 + } 46 + 47 + for (const id in input) { 48 + if (id.length > 32) { 49 + return v.err({ 50 + message: `service id too long (max 32 characters)`, 51 + path: [id], 52 + }); 53 + } 54 + } 55 + 56 + return v.ok(input); 57 + }), 111 58 }); 112 - export type PlcUpdatePayload = v.Infer<typeof updatePayload>; 59 + 60 + export type UpdatePayload = v.Infer<typeof updatePayload>;
-14
src/api/types/strings.ts
··· 1 1 import * as v from '@badrap/valita'; 2 2 3 - import { DID_KEY_RE, DID_RE, HANDLE_RE } from '../utils/strings'; 4 - 5 - export const didString = v 6 - .string() 7 - .assert((input): input is `did:${string}:${string}` => DID_RE.test(input), `must be a valid did`); 8 - 9 - export const didKeyString = v 10 - .string() 11 - .assert((input): input is `did:key:${string}` => DID_KEY_RE.test(input), `must be a valid did:key`); 12 - 13 - export const handleString = v.string().assert((input) => HANDLE_RE.test(input), `must be a valid handle`); 14 - 15 - export const urlString = v.string().assert((input) => URL.canParse(input), `must be a valid url`); 16 - 17 3 export const serviceUrlString = v.string().assert((input) => { 18 4 const url = URL.parse(input); 19 5
+53
src/api/utils/at-uri.ts
··· 1 + import { assert } from '~/lib/utils/invariant'; 2 + 3 + type Did<TMethod extends string = string> = `did:${TMethod}:${string}`; 4 + 5 + type Nsid = `${string}.${string}.${string}`; 6 + 7 + type RecordKey = string; 8 + 9 + const DID_RE = /^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$/; 10 + 11 + const NSID_RE = 12 + /^[a-zA-Z](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)+(?:\.[a-zA-Z](?:[a-zA-Z0-9]{0,62})?)$/; 13 + 14 + const RECORD_KEY_RE = /^(?!\.{1,2}$)[a-zA-Z0-9_~.:-]{1,512}$/; 15 + 16 + const ATURI_RE = 17 + /^at:\/\/([a-zA-Z0-9._:%-]+)(?:\/([a-zA-Z0-9-.]+)(?:\/([a-zA-Z0-9._~:@!$&%')(*+,;=-]+))?)?(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 18 + 19 + const isDid = (input: unknown): input is Did => { 20 + return typeof input === 'string' && input.length >= 7 && input.length <= 2048 && DID_RE.test(input); 21 + }; 22 + 23 + const isNsid = (input: unknown): input is Nsid => { 24 + return typeof input === 'string' && input.length >= 5 && input.length <= 317 && NSID_RE.test(input); 25 + }; 26 + 27 + const isRecordKey = (input: unknown): input is RecordKey => { 28 + return typeof input === 'string' && input.length >= 1 && input.length <= 512 && RECORD_KEY_RE.test(input); 29 + }; 30 + 31 + export interface AddressedAtUri { 32 + repo: Did; 33 + collection: Nsid; 34 + rkey: string; 35 + fragment: string | undefined; 36 + } 37 + 38 + export const parseAddressedAtUri = (str: string): AddressedAtUri => { 39 + const match = ATURI_RE.exec(str); 40 + assert(match !== null, `invalid addressed-at-uri: ${str}`); 41 + 42 + const [, r, c, k, f] = match; 43 + assert(isDid(r), `invalid repo in addressed-at-uri: ${r}`); 44 + assert(isNsid(c), `invalid collection in addressed-at-uri: ${c}`); 45 + assert(isRecordKey(k), `invalid rkey in addressed-at-uri: ${k}`); 46 + 47 + return { 48 + repo: r, 49 + collection: c, 50 + rkey: k, 51 + fragment: f, 52 + }; 53 + };
+1 -43
src/api/utils/strings.ts
··· 1 - import type { At, Records } from '@atcute/client/lexicons'; 2 - 3 - import { assert } from '~/lib/utils/invariant'; 4 - 5 - export const ATURI_RE = 6 - /^at:\/\/(did:[a-zA-Z0-9._:%\-]+|[a-zA-Z0-9-.]+)\/([a-zA-Z0-9-.]+)\/([a-zA-Z0-9._~:@!$&%')(*+,;=\-]+)(?:#(\/[a-zA-Z0-9._~:@!$&%')(*+,;=\-[\]/\\]*))?$/; 7 - 8 - export const DID_RE = /^did:([a-z]+):([a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-])$/; 9 - 10 - export const DID_WEB_RE = /^did:web:([a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*(?:\.[a-zA-Z]{2,}))$/; 11 - 12 - export const DID_PLC_RE = /^did:plc:([a-z2-7]{24})$/; 13 - 14 - export const DID_KEY_RE = /^did:key:z[a-km-zA-HJ-NP-Z1-9]+$/; 15 - 16 - export const HANDLE_RE = /^[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*(?:\.[a-zA-Z]{2,})$/; 1 + import type { Records } from '@atcute/client/lexicons'; 17 2 18 3 export const DID_OR_HANDLE_RE = 19 4 /^[a-zA-Z0-9\-]+(?:\.[a-zA-Z0-9\-]+)*(?:\.[a-zA-Z]{2,})$|^did:[a-z]+:[a-zA-Z0-9._:%\-]*[a-zA-Z0-9._\-]$/; 20 - 21 - export interface AtUri { 22 - repo: string; 23 - collection: string; 24 - rkey: string; 25 - fragment: string | undefined; 26 - } 27 - 28 - export const isDid = (value: string): value is At.DID => { 29 - return value.length >= 7 && DID_RE.test(value); 30 - }; 31 - 32 - export const isHandle = (value: string): boolean => { 33 - return value.length >= 4 && HANDLE_RE.test(value); 34 - }; 35 - 36 - export const parseAtUri = (str: string): AtUri => { 37 - const match = ATURI_RE.exec(str); 38 - assert(match !== null, `Failed to parse AT URI for ${str}`); 39 - 40 - return { 41 - repo: match[1], 42 - collection: match[2], 43 - rkey: match[3], 44 - fragment: match[4], 45 - }; 46 - }; 47 5 48 6 export const makeAtUri = (repo: string, collection: keyof Records | (string & {}), rkey: string) => { 49 7 return `at://${repo}/${collection}/${rkey}`;
+26
src/api/utils/valita.ts
··· 1 + import * as v from '@badrap/valita'; 2 + 3 + export type ToValidator<T> = T extends readonly [infer Head, ...infer Rest] 4 + ? Rest extends [] 5 + ? v.TupleType<[ToValidator<Head>]> 6 + : v.TupleType<[ToValidator<Head>, ...ToValidatorTuple<Rest>]> 7 + : T extends ReadonlyArray<infer E> 8 + ? v.ArrayType<ToValidator<E>> 9 + : T extends object 10 + ? ToObjectValidator<T> 11 + : v.Type<T>; 12 + 13 + // Helper type for converting tuple types 14 + type ToValidatorTuple<T extends readonly unknown[]> = T extends readonly [infer Head, ...infer Rest] 15 + ? Rest extends [] 16 + ? [ToValidator<Head>] 17 + : [ToValidator<Head>, ...ToValidatorTuple<Rest>] 18 + : []; 19 + 20 + // Helper type for converting object types 21 + type ToObjectValidator<T extends object> = v.ObjectType< 22 + { 23 + [K in keyof T]-?: undefined extends T[K] ? v.Optional<Exclude<T[K], undefined>> : ToValidator<T[K]>; 24 + }, 25 + undefined 26 + >;
+6 -6
src/components/wizards/bluesky-login-step.tsx
··· 1 1 import { batch, createSignal, Match, Show, Switch } from 'solid-js'; 2 2 3 3 import { CredentialManager, XRPCError } from '@atcute/client'; 4 - import { At } from '@atcute/client/lexicons'; 4 + import { type AtprotoDid, type DidDocument, getPdsEndpoint, isAtprotoDid, isHandle } from '@atcute/identity'; 5 5 6 6 import { getDidDocument } from '~/api/queries/did-doc'; 7 7 import { resolveHandleViaAppView } from '~/api/queries/handle'; 8 - import { DidDocument, getPdsEndpoint } from '~/api/types/did-doc'; 9 8 import { formatTotpCode, TOTP_RE } from '~/api/utils/auth'; 10 - import { isDid } from '~/api/utils/strings'; 11 9 12 10 import { createMutation } from '~/lib/utils/mutation'; 13 11 ··· 54 52 service = service?.trim() || undefined; 55 53 56 54 if (service === undefined) { 57 - let did: At.DID; 58 - if (!isDid(identifier)) { 55 + let did: AtprotoDid; 56 + if (isAtprotoDid(identifier)) { 57 + did = identifier; 58 + } else if (isHandle(identifier)) { 59 59 did = await resolveHandleViaAppView({ handle: identifier }); 60 60 } else { 61 - did = identifier; 61 + throw new InsufficientLoginError(`Invalid identifier`); 62 62 } 63 63 64 64 const didDoc = await getDidDocument({ did });
+4 -4
src/lib/utils/search-params.ts
··· 1 1 import { batch, createSignal } from 'solid-js'; 2 2 3 3 import { At } from '@atcute/client/lexicons'; 4 + import { isDid, isHandle } from '@atcute/identity'; 4 5 5 - import { DID_OR_HANDLE_RE, DID_RE, HANDLE_RE } from '~/api/utils/strings'; 6 6 import { UnwrapArray } from '~/api/utils/types'; 7 7 8 8 export interface ParamParser<T> { ··· 223 223 224 224 export const asDID = createParser({ 225 225 parse(value) { 226 - if (typeof value === 'string' && DID_RE.test(value)) { 226 + if (typeof value === 'string' && isDid(value)) { 227 227 return value as At.DID; 228 228 } 229 229 ··· 236 236 237 237 export const asHandle = createParser({ 238 238 parse(value) { 239 - if (typeof value === 'string' && HANDLE_RE.test(value)) { 239 + if (typeof value === 'string' && isHandle(value)) { 240 240 return value; 241 241 } 242 242 ··· 249 249 250 250 export const asIdentifier = createParser({ 251 251 parse(value) { 252 - if (typeof value === 'string' && DID_OR_HANDLE_RE.test(value)) { 252 + if (typeof value === 'string' && (isDid(value) || isHandle(value))) { 253 253 return value; 254 254 } 255 255
+14 -10
src/views/blob/blob-export.tsx
··· 2 2 import { createSignal } from 'solid-js'; 3 3 4 4 import { simpleFetchHandler, XRPC, XRPCError } from '@atcute/client'; 5 - import { At } from '@atcute/client/lexicons'; 5 + import { type AtprotoDid, getPdsEndpoint, isAtprotoDid, isHandle } from '@atcute/identity'; 6 6 import { writeTarEntry } from '@mary/tar'; 7 7 8 8 import { getDidDocument } from '~/api/queries/did-doc'; 9 9 import { resolveHandleViaAppView, resolveHandleViaPds } from '~/api/queries/handle'; 10 - import { getPdsEndpoint } from '~/api/types/did-doc'; 11 10 import { isServiceUrlString } from '~/api/types/strings'; 12 - import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 11 + import { DID_OR_HANDLE_RE } from '~/api/utils/strings'; 13 12 14 13 import { useTitle } from '~/lib/navigation/router'; 15 14 import { makeAbortable } from '~/lib/utils/abortable'; ··· 36 35 }) => { 37 36 logger.info(`Starting export for ${identifier}`); 38 37 39 - let did: At.DID; 40 - if (isDid(identifier)) { 38 + let did: AtprotoDid; 39 + if (isAtprotoDid(identifier)) { 41 40 did = identifier; 42 - } else if (service) { 43 - did = await resolveHandleViaPds({ service, handle: identifier, signal }); 44 - logger.log(`Resolved handle to ${did}`); 41 + } else if (isHandle(identifier)) { 42 + if (service) { 43 + did = await resolveHandleViaPds({ service, handle: identifier, signal }); 44 + logger.log(`Resolved handle to ${did}`); 45 + } else { 46 + did = await resolveHandleViaAppView({ handle: identifier, signal }); 47 + logger.log(`Resolved handle to ${did}`); 48 + } 45 49 } else { 46 - did = await resolveHandleViaAppView({ handle: identifier, signal }); 47 - logger.log(`Resolved handle to ${did}`); 50 + logger.error(`Invalid identifier`); 51 + return; 48 52 } 49 53 50 54 if (!service) {
+1 -1
src/views/bluesky/threadgate-applicator/page.tsx
··· 2 2 3 3 import { CredentialManager } from '@atcute/client'; 4 4 import { AppBskyFeedDefs, AppBskyFeedThreadgate } from '@atcute/client/lexicons'; 5 + import { DidDocument } from '@atcute/identity'; 5 6 6 - import { DidDocument } from '~/api/types/did-doc'; 7 7 import { UnwrapArray } from '~/api/utils/types'; 8 8 9 9 import { history } from '~/globals/navigation';
+8 -5
src/views/bluesky/threadgate-applicator/steps/step1_handle-input.tsx
··· 1 1 import { createSignal } from 'solid-js'; 2 2 3 - import type { AppBskyFeedThreadgate, At } from '@atcute/client/lexicons'; 3 + import type { AppBskyFeedThreadgate } from '@atcute/client/lexicons'; 4 + import { type AtprotoDid, isAtprotoDid, isHandle } from '@atcute/identity'; 4 5 5 6 import { getDidDocument } from '~/api/queries/did-doc'; 6 7 import { resolveHandleViaAppView } from '~/api/queries/handle'; 7 - import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 8 + import { DID_OR_HANDLE_RE } from '~/api/utils/strings'; 8 9 9 10 import { appViewRpc } from '~/globals/rpc'; 10 11 ··· 32 33 async mutationFn({ identifier }: { identifier: string }, signal) { 33 34 setStatus(`Resolving identity`); 34 35 35 - let did: At.DID; 36 - if (isDid(identifier)) { 36 + let did: AtprotoDid; 37 + if (isAtprotoDid(identifier)) { 37 38 did = identifier; 38 - } else { 39 + } else if (isHandle(identifier)) { 39 40 did = await resolveHandleViaAppView({ handle: identifier, signal }); 41 + } else { 42 + throw new Error(`Invalid identifier`); 40 43 } 41 44 42 45 const didDoc = await getDidDocument({ did, signal });
+7 -6
src/views/bluesky/threadgate-applicator/steps/step4_confirmation.tsx
··· 1 1 import { createSignal, Show } from 'solid-js'; 2 2 3 3 import { XRPC, XRPCError } from '@atcute/client'; 4 - import { AppBskyFeedThreadgate, ComAtprotoRepoApplyWrites } from '@atcute/client/lexicons'; 4 + import type { AppBskyFeedThreadgate, ComAtprotoRepoApplyWrites } from '@atcute/client/lexicons'; 5 5 import { chunked } from '@mary/array-fns'; 6 + 7 + import { parseAddressedAtUri } from '~/api/utils/at-uri'; 6 8 7 9 import { dequal } from '~/lib/utils/dequal'; 8 10 import { createMutation } from '~/lib/utils/mutation'; 9 11 10 12 import Button from '~/components/inputs/button'; 11 13 import ToggleInput from '~/components/inputs/toggle-input'; 14 + import Logger, { createLogger } from '~/components/logger'; 12 15 import { Stage, StageActions, StageErrorView, WizardStepProps } from '~/components/wizard'; 13 16 14 - import { parseAtUri } from '~/api/utils/strings'; 15 - import Logger, { createLogger } from '~/components/logger'; 16 17 import { ThreadgateApplicatorConstraints } from '../page'; 17 18 18 19 const Step4_Confirmation = ({ ··· 39 40 for (const { post, threadgate } of data.threads) { 40 41 if (threadgate === null) { 41 42 if (rules !== undefined) { 42 - const { rkey } = parseAtUri(post.uri); 43 + const { rkey } = parseAddressedAtUri(post.uri); 43 44 44 45 const record: AppBskyFeedThreadgate.Record = { 45 46 $type: 'app.bsky.feed.threadgate', ··· 58 59 } 59 60 } else { 60 61 if (rules === undefined && !threadgate.hiddenReplies?.length) { 61 - const { rkey } = parseAtUri(threadgate.uri); 62 + const { rkey } = parseAddressedAtUri(threadgate.uri); 62 63 63 64 writes.push({ 64 65 $type: 'com.atproto.repo.applyWrites#delete', ··· 66 67 rkey: rkey, 67 68 }); 68 69 } else if (!dequal(threadgate.allow, rules)) { 69 - const { rkey } = parseAtUri(threadgate.uri); 70 + const { rkey } = parseAddressedAtUri(threadgate.uri); 70 71 71 72 const record: AppBskyFeedThreadgate.Record = { 72 73 $type: 'app.bsky.feed.threadgate',
+11 -13
src/views/identity/did-lookup.tsx
··· 1 1 import { Match, Switch } from 'solid-js'; 2 2 3 - import { At } from '@atcute/client/lexicons'; 3 + import { isAtprotoDid, isHandle, type AtprotoDid, type Did, type Handle } from '@atcute/identity'; 4 4 5 5 import { getDidDocument } from '~/api/queries/did-doc'; 6 6 import { resolveHandleViaAppView } from '~/api/queries/handle'; 7 7 import { isServiceUrlString } from '~/api/types/strings'; 8 - import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 8 + import { DID_OR_HANDLE_RE } from '~/api/utils/strings'; 9 9 10 10 import { useTitle } from '~/lib/navigation/router'; 11 11 import { createQuery } from '~/lib/utils/query'; ··· 24 24 const query = createQuery( 25 25 () => params.q, 26 26 async (identifier, signal) => { 27 - let did: At.DID; 28 - if (isDid(identifier)) { 27 + let did: AtprotoDid; 28 + if (isAtprotoDid(identifier)) { 29 29 did = identifier; 30 - } else { 30 + } else if (isHandle(identifier)) { 31 31 did = await resolveHandleViaAppView({ handle: identifier, signal }); 32 + } else { 33 + throw new Error(`Invalid identifier`); 32 34 } 33 35 34 36 const doc = await getDidDocument({ did, signal }); ··· 55 57 const formData = new FormData(ev.currentTarget); 56 58 ev.preventDefault(); 57 59 58 - const ident = formData.get('ident') as string; 60 + const ident = formData.get('ident') as Did | Handle; 59 61 setParams({ q: ident }); 60 62 }} 61 63 class="m-4 flex flex-col gap-4" ··· 99 101 100 102 <div> 101 103 <p class="font-semibold text-gray-600">Identifies as</p> 102 - <ol class="list-disc pl-4"> 103 - {doc.alsoKnownAs.map((ident) => ( 104 - <li>{ident}</li> 105 - ))} 106 - </ol> 104 + <ol class="list-disc pl-4">{doc.alsoKnownAs?.map((ident) => <li>{ident}</li>)}</ol> 107 105 </div> 108 106 109 107 <div> 110 108 <p class="font-semibold text-gray-600">Services</p> 111 109 <ol class="list-disc pl-4"> 112 - {doc.service.map(({ id, type, serviceEndpoint }, idx) => { 110 + {doc.service?.map(({ id, type, serviceEndpoint }, idx) => { 113 111 const isString = typeof serviceEndpoint === 'string'; 114 112 const isURL = isString && URL.canParse('' + serviceEndpoint); 115 113 const isServiceUrl = isString && isServiceUrlString(serviceEndpoint); ··· 167 165 <div> 168 166 <p class="font-semibold text-gray-600">Verification methods</p> 169 167 <ol class="list-disc pl-4"> 170 - {doc.verificationMethod.map(({ id, type, publicKeyMultibase }, idx) => { 168 + {doc.verificationMethod?.map(({ id, type, publicKeyMultibase }, idx) => { 171 169 return ( 172 170 <li class={idx !== 0 ? `mt-3` : ``}> 173 171 <p class="font-medium">{id.replace(doc.id, '')}</p>
+4 -4
src/views/identity/plc-applicator/page.tsx
··· 3 3 import type { CredentialManager } from '@atcute/client'; 4 4 import type { ComAtprotoIdentityGetRecommendedDidCredentials } from '@atcute/client/lexicons'; 5 5 import type { P256PrivateKey, Secp256k1PrivateKey } from '@atcute/crypto'; 6 + import type { DidDocument } from '@atcute/identity'; 6 7 7 - import type { DidDocument } from '~/api/types/did-doc'; 8 - import type { PlcUpdatePayload } from '~/api/types/plc'; 8 + import { UpdatePayload } from '~/api/types/plc'; 9 9 10 10 import { history } from '~/globals/navigation'; 11 11 ··· 69 69 info: PlcInformation; 70 70 method: PdsSigningMethod; 71 71 base: DetailedPlcEntry; 72 - payload: PlcUpdatePayload; 72 + payload: UpdatePayload; 73 73 }; 74 74 Step5_PrivateKeyConfirmation: { 75 75 info: PlcInformation; 76 76 method: PrivateKeySigningMethod; 77 77 base: DetailedPlcEntry; 78 - payload: PlcUpdatePayload; 78 + payload: UpdatePayload; 79 79 }; 80 80 81 81 Step6_Finished: {};
+5 -4
src/views/identity/plc-applicator/plc-utils.ts
··· 1 1 import * as CBOR from '@atcute/cbor'; 2 2 import { verifySigWithDidKey } from '@atcute/crypto'; 3 + import type { IndexedEntry } from '@atcute/did-plc'; 3 4 import { fromBase64Url } from '@atcute/multibase'; 4 5 5 - import { PlcLogEntry, PlcUpdatePayload } from '~/api/types/plc'; 6 + import { UpdatePayload } from '~/api/types/plc'; 6 7 import { UnwrapArray } from '~/api/utils/types'; 7 8 8 9 import { assert } from '~/lib/utils/invariant'; 9 10 10 - export const getPlcPayload = (entry: PlcLogEntry): PlcUpdatePayload => { 11 + export const getPlcPayload = (entry: IndexedEntry): UpdatePayload => { 11 12 const op = entry.operation; 12 13 assert(op.type === 'plc_operation' || op.type === 'create'); 13 14 ··· 37 38 assert(false); 38 39 }; 39 40 40 - export const getPlcKeying = async (logs: PlcLogEntry[]) => { 41 + export const getPlcKeying = async (logs: IndexedEntry[]) => { 41 42 logs = logs.filter((entry) => !entry.nullified); 42 43 43 44 const length = logs.length; ··· 112 113 type DetailedEntries = Awaited<ReturnType<typeof getPlcKeying>>; 113 114 export type DetailedPlcEntry = UnwrapArray<DetailedEntries>; 114 115 115 - export const getCurrentSignersFromEntry = (entry: PlcLogEntry): string[] => { 116 + export const getCurrentSignersFromEntry = (entry: IndexedEntry): string[] => { 116 117 const operation = entry.operation; 117 118 118 119 /** keys that can sign the next operation */
+12 -9
src/views/identity/plc-applicator/steps/step1_handle-input.tsx
··· 1 1 import { createSignal } from 'solid-js'; 2 2 3 3 import { XRPCError } from '@atcute/client'; 4 - import { At } from '@atcute/client/lexicons'; 4 + import { type Did, isHandle, isPlcDid } from '@atcute/identity'; 5 5 6 6 import { getDidDocument } from '~/api/queries/did-doc'; 7 7 import { resolveHandleViaAppView } from '~/api/queries/handle'; 8 8 import { getPlcAuditLogs } from '~/api/queries/plc'; 9 - import { DID_OR_HANDLE_RE, DID_PLC_RE, isDid } from '~/api/utils/strings'; 9 + import { DID_OR_HANDLE_RE } from '~/api/utils/strings'; 10 10 11 11 import { createMutation } from '~/lib/utils/mutation'; 12 12 ··· 38 38 39 39 const mutation = createMutation({ 40 40 async mutationFn({ identifier }: MutationVariables): Promise<PlcInformation> { 41 - let did: At.DID; 42 - if (isDid(identifier)) { 41 + let did: Did<'plc'>; 42 + if (isPlcDid(identifier)) { 43 43 did = identifier; 44 - } else { 45 - did = await resolveHandleViaAppView({ handle: identifier }); 46 - } 44 + } else if (isHandle(identifier)) { 45 + const resolved = await resolveHandleViaAppView({ handle: identifier }); 46 + if (!isPlcDid(resolved)) { 47 + throw new DidIsNotPlcError(`${resolved} does not resolve to a did:plc`); 48 + } 47 49 48 - if (!DID_PLC_RE.test(did)) { 49 - throw new DidIsNotPlcError(`"${did}" is not did:plc`); 50 + did = resolved; 51 + } else { 52 + throw new DidIsNotPlcError(`${identifier} is not a valid did:plc or handle`); 50 53 } 51 54 52 55 const [didDoc, logs] = await Promise.all([getDidDocument({ did }), getPlcAuditLogs({ did })]);
+2 -2
src/views/identity/plc-applicator/steps/step2_pds-authentication.tsx
··· 2 2 3 3 import { AtpAccessJwt, CredentialManager, XRPC, XRPCError } from '@atcute/client'; 4 4 import { decodeJwt } from '@atcute/client/utils/jwt'; 5 + import { getPdsEndpoint } from '@atcute/identity'; 5 6 6 - import { getPdsEndpoint } from '~/api/types/did-doc'; 7 - import { TOTP_RE, formatTotpCode } from '~/api/utils/auth'; 7 + import { formatTotpCode, TOTP_RE } from '~/api/utils/auth'; 8 8 9 9 import { createMutation } from '~/lib/utils/mutation'; 10 10
+4 -5
src/views/identity/plc-applicator/steps/step5_private-key-confirmation.tsx
··· 1 1 import { createSignal } from 'solid-js'; 2 2 3 3 import * as CBOR from '@atcute/cbor'; 4 + import type { Operation, UnsignedOperation } from '@atcute/did-plc'; 4 5 import { toBase64Url } from '@atcute/multibase'; 5 - 6 - import { PlcUpdateOp } from '~/api/types/plc'; 7 6 8 7 import { generateConfirmationCode } from '~/lib/utils/confirmation-code'; 9 8 import { createMutation } from '~/lib/utils/mutation'; ··· 30 29 const payload = data.payload; 31 30 const prev = data.base; 32 31 33 - const operation: Omit<PlcUpdateOp, 'sig'> = { 32 + const operation: UnsignedOperation = { 34 33 type: 'plc_operation', 35 34 prev: prev!.cid, 36 35 ··· 45 44 46 45 const signature = toBase64Url(sigBytes); 47 46 48 - const signedOperation: PlcUpdateOp = { 47 + const signedOperation: Operation = { 49 48 ...operation, 50 49 sig: signature, 51 50 }; ··· 122 121 123 122 export default Step5_PrivateKeyConfirmation; 124 123 125 - const pushPlcOperation = async (did: string, operation: PlcUpdateOp) => { 124 + const pushPlcOperation = async (did: string, operation: Operation) => { 126 125 const origin = import.meta.env.VITE_PLC_DIRECTORY_URL; 127 126 const response = await fetch(`${origin}/${did}`, { 128 127 method: 'post',
+28 -25
src/views/identity/plc-oplogs.tsx
··· 1 1 import { createSignal, JSX, Match, onCleanup, Switch } from 'solid-js'; 2 2 3 - import { At } from '@atcute/client/lexicons'; 3 + import type { IndexedEntry, Service } from '@atcute/did-plc'; 4 + import { type Did, type Handle, isHandle, isPlcDid } from '@atcute/identity'; 4 5 5 6 import { resolveHandleViaAppView } from '~/api/queries/handle'; 6 - import { PlcLogEntry, Service } from '~/api/types/plc'; 7 - import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 7 + import { DID_OR_HANDLE_RE } from '~/api/utils/strings'; 8 8 9 9 import { getPlcAuditLogs } from '~/api/queries/plc'; 10 10 import { useTitle } from '~/lib/navigation/router'; ··· 29 29 const query = createQuery( 30 30 () => params.q, 31 31 async (identifier, signal) => { 32 - let did: At.DID; 33 - if (isDid(identifier)) { 32 + let did: Did<'plc'>; 33 + if (isPlcDid(identifier)) { 34 34 did = identifier; 35 - } else { 36 - did = await resolveHandleViaAppView({ handle: identifier, signal }); 37 - } 35 + } else if (isHandle(identifier)) { 36 + const resolved = await resolveHandleViaAppView({ handle: identifier, signal }); 37 + if (!isPlcDid(resolved)) { 38 + throw new Error(`${identifier} is not a valid identifier`); 39 + } 38 40 39 - if (!did.startsWith('did:plc:')) { 40 - throw new Error(`${did} is not plc`); 41 + did = resolved; 42 + } else { 43 + throw new Error(`${identifier} is not a valid identifier`); 41 44 } 42 45 43 46 const logs = await getPlcAuditLogs({ did, signal }); ··· 63 66 const formData = new FormData(ev.currentTarget); 64 67 ev.preventDefault(); 65 68 66 - const ident = formData.get('ident') as string; 69 + const ident = formData.get('ident') as Did | Handle; 67 70 setParams({ q: ident }); 68 71 }} 69 72 class="m-4 flex flex-col gap-4" ··· 372 375 type DiffEntry = 373 376 | { 374 377 type: 'identity_created'; 375 - orig: PlcLogEntry; 378 + orig: IndexedEntry; 376 379 nullified: boolean; 377 380 at: string; 378 381 rotationKeys: string[]; ··· 382 385 } 383 386 | { 384 387 type: 'identity_tombstoned'; 385 - orig: PlcLogEntry; 388 + orig: IndexedEntry; 386 389 nullified: boolean; 387 390 at: string; 388 391 } 389 392 | { 390 393 type: 'rotation_key_added'; 391 - orig: PlcLogEntry; 394 + orig: IndexedEntry; 392 395 nullified: boolean; 393 396 at: string; 394 397 rotation_key: string; 395 398 } 396 399 | { 397 400 type: 'rotation_key_removed'; 398 - orig: PlcLogEntry; 401 + orig: IndexedEntry; 399 402 nullified: boolean; 400 403 at: string; 401 404 rotation_key: string; 402 405 } 403 406 | { 404 407 type: 'verification_method_added'; 405 - orig: PlcLogEntry; 408 + orig: IndexedEntry; 406 409 nullified: boolean; 407 410 at: string; 408 411 method_id: string; ··· 410 413 } 411 414 | { 412 415 type: 'verification_method_removed'; 413 - orig: PlcLogEntry; 416 + orig: IndexedEntry; 414 417 nullified: boolean; 415 418 at: string; 416 419 method_id: string; ··· 418 421 } 419 422 | { 420 423 type: 'verification_method_changed'; 421 - orig: PlcLogEntry; 424 + orig: IndexedEntry; 422 425 nullified: boolean; 423 426 at: string; 424 427 method_id: string; ··· 427 430 } 428 431 | { 429 432 type: 'handle_added'; 430 - orig: PlcLogEntry; 433 + orig: IndexedEntry; 431 434 nullified: boolean; 432 435 at: string; 433 436 handle: string; 434 437 } 435 438 | { 436 439 type: 'handle_removed'; 437 - orig: PlcLogEntry; 440 + orig: IndexedEntry; 438 441 nullified: boolean; 439 442 at: string; 440 443 handle: string; 441 444 } 442 445 | { 443 446 type: 'handle_changed'; 444 - orig: PlcLogEntry; 447 + orig: IndexedEntry; 445 448 nullified: boolean; 446 449 at: string; 447 450 prev_handle: string; ··· 449 452 } 450 453 | { 451 454 type: 'service_added'; 452 - orig: PlcLogEntry; 455 + orig: IndexedEntry; 453 456 nullified: boolean; 454 457 at: string; 455 458 service_id: string; ··· 458 461 } 459 462 | { 460 463 type: 'service_removed'; 461 - orig: PlcLogEntry; 464 + orig: IndexedEntry; 462 465 nullified: boolean; 463 466 at: string; 464 467 service_id: string; ··· 467 470 } 468 471 | { 469 472 type: 'service_changed'; 470 - orig: PlcLogEntry; 473 + orig: IndexedEntry; 471 474 nullified: boolean; 472 475 at: string; 473 476 service_id: string; ··· 477 480 next_service_endpoint: string; 478 481 }; 479 482 480 - const createOperationHistory = (entries: PlcLogEntry[]): DiffEntry[] => { 483 + const createOperationHistory = (entries: IndexedEntry[]): DiffEntry[] => { 481 484 const history: DiffEntry[] = []; 482 485 483 486 for (let idx = 0, len = entries.length; idx < len; idx++) {
+14 -10
src/views/repository/repo-export.tsx
··· 1 1 import { type FileSystemFileHandle, showSaveFilePicker } from 'native-file-system-adapter'; 2 2 import { createSignal } from 'solid-js'; 3 3 4 - import { At } from '@atcute/client/lexicons'; 4 + import { type AtprotoDid, getPdsEndpoint, isAtprotoDid, isHandle } from '@atcute/identity'; 5 5 6 6 import { getDidDocument } from '~/api/queries/did-doc'; 7 7 import { resolveHandleViaAppView, resolveHandleViaPds } from '~/api/queries/handle'; 8 - import { getPdsEndpoint } from '~/api/types/did-doc'; 9 8 import { isServiceUrlString } from '~/api/types/strings'; 10 - import { DID_OR_HANDLE_RE, isDid } from '~/api/utils/strings'; 9 + import { DID_OR_HANDLE_RE } from '~/api/utils/strings'; 11 10 12 11 import { useTitle } from '~/lib/navigation/router'; 13 12 import { makeAbortable } from '~/lib/utils/abortable'; ··· 34 33 }) => { 35 34 logger.info(`Starting export for ${identifier}`); 36 35 37 - let did: At.DID; 38 - if (isDid(identifier)) { 36 + let did: AtprotoDid; 37 + if (isAtprotoDid(identifier)) { 39 38 did = identifier; 40 - } else if (service) { 41 - did = await resolveHandleViaPds({ service, handle: identifier, signal }); 42 - logger.log(`Resolved handle to ${did}`); 39 + } else if (isHandle(identifier)) { 40 + if (service) { 41 + did = await resolveHandleViaPds({ service, handle: identifier, signal }); 42 + logger.log(`Resolved handle to ${did}`); 43 + } else { 44 + did = await resolveHandleViaAppView({ handle: identifier, signal }); 45 + logger.log(`Resolved handle to ${did}`); 46 + } 43 47 } else { 44 - did = await resolveHandleViaAppView({ handle: identifier, signal }); 45 - logger.log(`Resolved handle to ${did}`); 48 + logger.error(`Invalid identifier`); 49 + return; 46 50 } 47 51 48 52 if (!service) {