Finish useBunkerStore?

environments/review-update-vid-g70vyz/deployments/5013
Alex Gleason 2024-10-28 15:27:07 -05:00
rodzic 14793ef0a9
commit eb0f5b8e3e
Nie znaleziono w bazie danych klucza dla tego podpisu
ID klucza GPG: 7211D1F99744FBB7
5 zmienionych plików z 166 dodań i 21 usunięć

Wyświetl plik

@ -155,7 +155,8 @@
"vite-plugin-html": "^3.2.2",
"vite-plugin-require": "^1.2.14",
"vite-plugin-static-copy": "^1.0.6",
"zod": "^3.23.5"
"zod": "^3.23.5",
"zustand": "^5.0.0"
},
"devDependencies": {
"@formatjs/cli": "^6.2.0",

Wyświetl plik

@ -37,7 +37,7 @@ function getSessionUser(): string | undefined {
/** Retrieve state from browser storage. */
function getLocalState(): SoapboxAuth | undefined {
const data = localStorage.getItem(STORAGE_KEY);
const result = jsonSchema.pipe(soapboxAuthSchema).safeParse(data);
const result = jsonSchema().pipe(soapboxAuthSchema).safeParse(data);
if (!result.success) {
return undefined;

Wyświetl plik

@ -1,4 +1,18 @@
import { createSlice } from '@reduxjs/toolkit';
import { produce } from 'immer';
import { generateSecretKey, getPublicKey, nip19 } from 'nostr-tools';
import { z } from 'zod';
import { create } from 'zustand';
// eslint-disable-next-line import/extensions
import { persist } from 'zustand/middleware';
import { filteredArray, jsonSchema } from 'soapbox/schemas/utils';
/** User-facing authorization string. */
interface BunkerURI {
pubkey: string;
relays: string[];
secret?: string;
}
/**
* Temporary authorization details to establish a bunker connection with an app.
@ -13,7 +27,7 @@ interface BunkerAuthorization {
/** User pubkey. Events will be signed by this pubkey. */
pubkey: string;
/** Secret key for this connection. NIP-46 responses will be signed by this key. */
bunkerSeckey: `nsec1${string}`;
bunkerSeckey: Uint8Array;
}
/**
@ -29,14 +43,137 @@ interface BunkerConnection {
/** Pubkey of the app authorized to sign events with this connection. */
authorizedPubkey: string;
/** Secret key for this connection. NIP-46 responses will be signed by this key. */
bunkerSeckey: `nsec1${string}`;
bunkerSeckey: Uint8Array;
}
export default createSlice({
name: 'bunker',
initialState: {
authorizations: [] as BunkerAuthorization[],
connections: [] as BunkerConnection[],
},
reducers: {},
});
/** Options for connecting to the bunker. */
interface BunkerConnectRequest {
accessToken: string;
authorizedPubkey: string;
bunkerPubkey: string;
secret: string;
}
const nsecSchema = z.custom<`nsec1${string}`>((v) => typeof v === 'string' && v.startsWith('nsec1'));
const connectionSchema = z.object({
pubkey: z.string(),
accessToken: z.string(),
authorizedPubkey: z.string(),
bunkerSeckey: nsecSchema,
});
const authorizationSchema = z.object({
secret: z.string(),
pubkey: z.string(),
bunkerSeckey: nsecSchema,
});
const stateSchema = z.object({
connections: filteredArray(connectionSchema),
authorizations: filteredArray(authorizationSchema),
});
interface BunkerState {
connections: BunkerConnection[];
authorizations: BunkerAuthorization[];
}
export const useBunkerStore = create<BunkerState>()(
persist(
(setState, getState) => ({
connections: [],
authorizations: [],
/** Generate a new authorization and persist it into the store. */
authorize(pubkey: string): BunkerURI {
const authorization: BunkerAuthorization = {
pubkey,
secret: crypto.randomUUID(),
bunkerSeckey: generateSecretKey(),
};
setState((state) => {
return produce(state, (draft) => {
draft.authorizations.push(authorization);
});
});
return {
pubkey: getPublicKey(authorization.bunkerSeckey),
secret: authorization.secret,
relays: [],
};
},
/** Connect to a bunker using the authorization secret. */
connect(request: BunkerConnectRequest): void {
const { authorizations } = getState();
const authorization = authorizations.find(
(existing) => existing.secret === request.secret && getPublicKey(existing.bunkerSeckey) === request.bunkerPubkey,
);
if (!authorization) {
throw new Error('Authorization not found');
}
const connection: BunkerConnection = {
pubkey: authorization.pubkey,
accessToken: request.accessToken,
authorizedPubkey: request.authorizedPubkey,
bunkerSeckey: authorization.bunkerSeckey,
};
setState((state) => {
return produce(state, (draft) => {
draft.connections.push(connection);
draft.authorizations = draft.authorizations.filter((existing) => existing !== authorization);
});
});
},
}),
{
name: 'soapbox:bunker',
storage: {
getItem(name) {
const connections = localStorage.getItem(`${name}:connections`);
const authorizations = sessionStorage.getItem(`${name}:authorizations`);
const state = stateSchema.parse({
connections: jsonSchema(nsecReviver).catch([]).parse(connections),
authorizations: jsonSchema(nsecReviver).catch([]).parse(authorizations),
});
return { state };
},
setItem(name, { state }) {
localStorage.setItem(`${name}:connections`, JSON.stringify(state.connections, nsecReplacer));
sessionStorage.setItem(`${name}:authorizations`, JSON.stringify(state.authorizations, nsecReplacer));
},
removeItem(name) {
localStorage.removeItem(`${name}:connections`);
sessionStorage.removeItem(`${name}:authorizations`);
},
},
},
),
);
/** Encode Uint8Arrays into nsec strings. */
function nsecReplacer(_key: string, value: unknown): unknown {
if (value instanceof Uint8Array) {
return nip19.nsecEncode(value);
}
return value;
}
/** Decode nsec strings into Uint8Arrays. */
function nsecReviver(_key: string, value: unknown): unknown {
if (typeof value === 'string' && value.startsWith('nsec1')) {
return nip19.decode(value as `nsec1${string}`).data;
}
return value;
}

Wyświetl plik

@ -30,14 +30,16 @@ function makeCustomEmojiMap(customEmojis: CustomEmoji[]) {
}, {});
}
const jsonSchema = z.string().transform((value, ctx) => {
try {
return JSON.parse(value) as unknown;
} catch (_e) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid JSON' });
return z.NEVER;
}
});
function jsonSchema(reviver?: (this: any, key: string, value: any) => any) {
return z.string().transform((value, ctx) => {
try {
return JSON.parse(value, reviver) as unknown;
} catch (_e) {
ctx.addIssue({ code: z.ZodIssueCode.custom, message: 'Invalid JSON' });
return z.NEVER;
}
});
}
/** MIME schema, eg `image/png`. */
const mimeSchema = z.string().regex(/^\w+\/[-+.\w]+$/);

Wyświetl plik

@ -9292,3 +9292,8 @@ zod@^3.23.4, zod@^3.23.5:
version "3.23.5"
resolved "https://registry.yarnpkg.com/zod/-/zod-3.23.5.tgz#c7b7617d017d4a2f21852f533258d26a9a5ae09f"
integrity sha512-fkwiq0VIQTksNNA131rDOsVJcns0pfVUjHzLrNBiF/O/Xxb5lQyEXkhZWcJ7npWsYlvs+h0jFWXXy4X46Em1JA==
zustand@^5.0.0:
version "5.0.0"
resolved "https://registry.yarnpkg.com/zustand/-/zustand-5.0.0.tgz#71f8aaecf185592a3ba2743d7516607361899da9"
integrity sha512-LE+VcmbartOPM+auOjCCLQOsQ05zUTp8RkgwRzefUk+2jISdMMFnxvyTjA4YNWr5ZGXYbVsEMZosttuxUBkojQ==