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