+3
-5
eslint.config.js
+3
-5
eslint.config.js
···
6
6
import react from 'eslint-plugin-react'
7
7
import reactHooks from 'eslint-plugin-react-hooks'
8
8
import tsdoc from 'eslint-plugin-tsdoc'
9
+
import {defineConfig} from 'eslint/config'
9
10
import globals from 'globals'
10
11
import path from 'node:path'
11
12
import tseslint from 'typescript-eslint'
12
13
13
14
const gitignore = path.resolve(import.meta.dirname, '.gitignore')
14
15
15
-
export default tseslint.config(
16
+
export default defineConfig(
16
17
includeIgnoreFile(gitignore, '.gitignore'),
17
18
18
19
// all files by default get shared globals
···
54
55
'@typescript-eslint/restrict-template-expressions': 'off',
55
56
56
57
// I need to be able to do `while(true)`, come on yall...
57
-
'@typescript-eslint/no-unnecessary-condition': [
58
-
'warn',
59
-
{allowConstantLoopConditions: 'always'},
60
-
],
58
+
'@typescript-eslint/no-unnecessary-condition': ['warn', {allowConstantLoopConditions: 'always'}],
61
59
62
60
// this breaks when I want to use a type parameter to allow type checking inline arguments
63
61
// it's "unnecessary type parameters" or an `as` declaration, which I don't like
+1
-1
prettier.config.js
+1
-1
prettier.config.js
+1
-3
src/client/components/feed-import-nytimes.tsx
+1
-3
src/client/components/feed-import-nytimes.tsx
···
113
113
return (
114
114
<div className="feed-import-nytimes">
115
115
<button type="button" onClick={importAll} disabled={importing$.value}>
116
-
{importing$.value
117
-
? `Importing... (${imported$.value}/${NYTIMES_FEEDS.length})`
118
-
: 'Import All NY Times Feeds'}
116
+
{importing$.value ? `Importing... (${imported$.value}/${NYTIMES_FEEDS.length})` : 'Import All NY Times Feeds'}
119
117
</button>
120
118
121
119
{errors$.value.length > 0 && (
+1
-3
src/client/components/feed-import-podcasts.tsx
+1
-3
src/client/components/feed-import-podcasts.tsx
···
61
61
return (
62
62
<div className="feed-import-podcasts">
63
63
<button type="button" onClick={importAll} disabled={importing$.value}>
64
-
{importing$.value
65
-
? `Importing... (${imported$.value}/${PODCAST_FEEDS.length})`
66
-
: 'Import Podcast Feeds'}
64
+
{importing$.value ? `Importing... (${imported$.value}/${PODCAST_FEEDS.length})` : 'Import Podcast Feeds'}
67
65
</button>
68
66
69
67
{errors$.value.length > 0 && (
+1
-3
src/client/components/feed-import-tech.tsx
+1
-3
src/client/components/feed-import-tech.tsx
···
60
60
return (
61
61
<div className="feed-import-tech">
62
62
<button type="button" onClick={importAll} disabled={importing$.value}>
63
-
{importing$.value
64
-
? `Importing... (${imported$.value}/${TECH_FEEDS.length})`
65
-
: 'Import Tech Feeds'}
63
+
{importing$.value ? `Importing... (${imported$.value}/${TECH_FEEDS.length})` : 'Import Tech Feeds'}
66
64
</button>
67
65
68
66
{errors$.value.length > 0 && (
+1
-5
src/client/page-app.tsx
+1
-5
src/client/page-app.tsx
···
22
22
const wsurl = `${wsproto}://${wshost}/stream`
23
23
24
24
return (
25
-
<SkypodProvider
26
-
identityFallback={identityFallback}
27
-
connectionFallback={connectionFallback}
28
-
websocketUrl={wsurl}
29
-
>
25
+
<SkypodProvider identityFallback={identityFallback} connectionFallback={connectionFallback} websocketUrl={wsurl}>
30
26
<RealmConnectionManager />
31
27
<PeerList />
32
28
<FeedImportNYTimes />
+2
-6
src/client/root/context-database.tsx
+2
-6
src/client/root/context-database.tsx
···
13
13
useDbSignal: <T>(querier: DbQuerier<T>) => ReadonlySignal<T | undefined>
14
14
}
15
15
16
-
export const DatabaseProvider: preact.FunctionComponent<{children: preact.ComponentChildren}> = (
17
-
props,
18
-
) => {
16
+
export const DatabaseProvider: preact.FunctionComponent<{children: preact.ComponentChildren}> = (props) => {
19
17
const db = useMemo(() => new Database(), [])
20
18
21
19
function useDbSignal<T>(querier: DbQuerier<T>): ReadonlySignal<T | undefined> {
···
32
30
return signal
33
31
}
34
32
35
-
return (
36
-
<DatabaseContext.Provider value={{db, useDbSignal}}>{props.children}</DatabaseContext.Provider>
37
-
)
33
+
return <DatabaseContext.Provider value={{db, useDbSignal}}>{props.children}</DatabaseContext.Provider>
38
34
}
39
35
40
36
export function useDatabase() {
+3
-14
src/client/skypod/action-dispatch/context.tsx
+3
-14
src/client/skypod/action-dispatch/context.tsx
···
8
8
9
9
import {Action, ActionMap, ActionOpts} from '#skypod/actions'
10
10
11
-
export type MiddlewareFn = (
12
-
this: undefined,
13
-
action: Action,
14
-
) => void | Action[] | Promise<void | Action[]>
11
+
export type MiddlewareFn = (this: undefined, action: Action) => void | Action[] | Promise<void | Action[]>
15
12
16
13
export type MiddlewareRouterFn<K extends keyof ActionMap> = (
17
14
this: undefined,
···
89
86
}
90
87
},
91
88
92
-
action: <N extends keyof ActionMap>(
93
-
msg: N,
94
-
dat: ActionMap[N]['dat'],
95
-
opt?: ActionOpts,
96
-
): ActionMap[N] => {
89
+
action: <N extends keyof ActionMap>(msg: N, dat: ActionMap[N]['dat'], opt?: ActionOpts): ActionMap[N] => {
97
90
const clk = identity.clock.now()
98
91
return {typ: 'act', clk, msg, dat, opt} as ActionMap[N]
99
92
},
···
135
128
},
136
129
})
137
130
138
-
return (
139
-
<ActionDispatchContext.Provider value={context.current}>
140
-
{props.children}
141
-
</ActionDispatchContext.Provider>
142
-
)
131
+
return <ActionDispatchContext.Provider value={context.current}>{props.children}</ActionDispatchContext.Provider>
143
132
}
144
133
145
134
export function useActionDispatch() {
+2
-14
src/client/skypod/feed-processor/worker.ts
+2
-14
src/client/skypod/feed-processor/worker.ts
···
47
47
48
48
async processPending() {
49
49
const pendingFeeds = this.#db.feeds.where('lastRefresh.status').equals('pending')
50
-
return await this.#db.withLock(
51
-
'feeds',
52
-
pendingFeeds,
53
-
this.#clock,
54
-
this.#owner,
55
-
this.#processLocked,
56
-
)
50
+
return await this.#db.withLock('feeds', pendingFeeds, this.#clock, this.#owner, this.#processLocked)
57
51
}
58
52
59
53
async processUrls(urls: string[]) {
60
54
const requestedFeeds = this.#db.feeds.where('url').anyOf(urls)
61
-
return await this.#db.withLock(
62
-
'feeds',
63
-
requestedFeeds,
64
-
this.#clock,
65
-
this.#owner,
66
-
this.#processLocked,
67
-
)
55
+
return await this.#db.withLock('feeds', requestedFeeds, this.#clock, this.#owner, this.#processLocked)
68
56
}
69
57
70
58
#poll = () => {
+1
-4
src/cmd/register-ident.ts
+1
-4
src/cmd/register-ident.ts
···
15
15
pubkey: await jwkExport.parseAsync(keypair.publicKey),
16
16
}
17
17
18
-
const jwt = await generateSignableJwt(payload)
19
-
.setIssuedAt()
20
-
.setExpirationTime('1m')
21
-
.sign(keypair.privateKey)
18
+
const jwt = await generateSignableJwt(payload).setIssuedAt().setExpirationTime('1m').sign(keypair.privateKey)
22
19
23
20
console.log('Generated Preauth JWT:')
24
21
console.log(jwt)
+5
-12
src/common/crypto/cipher.ts
+5
-12
src/common/crypto/cipher.ts
···
43
43
const nonce = encoder.encode(`${nonceStr}-nonce-cryptosystem`)
44
44
const derivedsalt = new Uint8Array([...salt, ...nonce])
45
45
46
-
return await crypto.subtle.deriveKey(
47
-
{...deriveAlgo, salt: derivedsalt, iterations},
48
-
derivedkey,
49
-
encrAlgo,
50
-
false,
51
-
['encrypt', 'decrypt'],
52
-
)
46
+
return await crypto.subtle.deriveKey({...deriveAlgo, salt: derivedsalt, iterations}, derivedkey, encrAlgo, false, [
47
+
'encrypt',
48
+
'decrypt',
49
+
])
53
50
}
54
51
55
52
/**
···
96
93
async encrypt(data: string | Uint8Array): Promise<string> {
97
94
const iv = crypto.getRandomValues(new Uint8Array(12))
98
95
const encoded = asUint8Array(data)
99
-
const encrypted = await crypto.subtle.encrypt(
100
-
{...encrAlgo, iv},
101
-
this.#cryptokey,
102
-
encoded as BufferSource,
103
-
)
96
+
const encrypted = await crypto.subtle.encrypt({...encrAlgo, iv}, this.#cryptokey, encoded as BufferSource)
104
97
105
98
// output = [iv + encrypted] which gives us the auth tag
106
99
+13
-15
src/common/crypto/jwts.ts
+13
-15
src/common/crypto/jwts.ts
···
20
20
* schema describing a decoded JWT.
21
21
* **important** - this does no claims validation, only decoding from string to JWT!
22
22
*/
23
-
export const jwtSchema: z.ZodType<JWTToken, string> = z
24
-
.jwt({abort: true})
25
-
.transform((token, ctx) => {
26
-
try {
27
-
const claims = jose.decodeJwt(token)
28
-
return {claims, token}
29
-
} catch (e) {
30
-
ctx.issues.push({
31
-
code: 'custom',
32
-
message: `error while decoding token: ${e}`,
33
-
input: token,
34
-
})
23
+
export const jwtSchema: z.ZodType<JWTToken, string> = z.jwt({abort: true}).transform((token, ctx) => {
24
+
try {
25
+
const claims = jose.decodeJwt(token)
26
+
return {claims, token}
27
+
} catch (e) {
28
+
ctx.issues.push({
29
+
code: 'custom',
30
+
message: `error while decoding token: ${e}`,
31
+
input: token,
32
+
})
35
33
36
-
return z.NEVER
37
-
}
38
-
})
34
+
return z.NEVER
35
+
}
36
+
})
39
37
40
38
/**
41
39
* schema describing a verified payload in a JWT.
+2
-9
src/common/socket.ts
+2
-9
src/common/socket.ts
···
66
66
}
67
67
68
68
/** exactly take socket, but will additionally apply a json decoding */
69
-
export async function takeSocketJson<T>(
70
-
ws: WebSocket,
71
-
schema: z.ZodType<T>,
72
-
signal?: AbortSignal,
73
-
): Promise<T> {
69
+
export async function takeSocketJson<T>(ws: WebSocket, schema: z.ZodType<T>, signal?: AbortSignal): Promise<T> {
74
70
const data = await takeSocket(ws, signal)
75
71
return parseJson.pipe(schema).parseAsync(data)
76
72
}
···
191
187
* exactly stream socket, but will additionally apply a json decoding
192
188
* messages not validating will end the stream with an error
193
189
*/
194
-
export async function* streamSocketJson(
195
-
ws: WebSocket,
196
-
config?: Partial<ConfigProps>,
197
-
): AsyncGenerator {
190
+
export async function* streamSocketJson(ws: WebSocket, config?: Partial<ConfigProps>): AsyncGenerator {
198
191
for await (const message of streamSocket(ws, config)) {
199
192
yield parseJson.parseAsync(message)
200
193
}
+1
-4
src/realm/client/components/connection-manager.tsx
+1
-4
src/realm/client/components/connection-manager.tsx
···
81
81
</p>
82
82
<p>
83
83
Exchange an invite:
84
-
<textarea
85
-
value={invitation.value}
86
-
onInput={(e) => (invitation.value = e.currentTarget.value)}
87
-
/>
84
+
<textarea value={invitation.value} onInput={(e) => (invitation.value = e.currentTarget.value)} />
88
85
<button type="button" onClick={exchange}>
89
86
Exchange
90
87
</button>
+1
-3
src/realm/client/context-identity.tsx
+1
-3
src/realm/client/context-identity.tsx
···
45
45
}, [store, value$, error$])
46
46
47
47
return value$.value ? (
48
-
<RealmIdentityContext.Provider value={value$.value}>
49
-
{props.children}
50
-
</RealmIdentityContext.Provider>
48
+
<RealmIdentityContext.Provider value={value$.value}>{props.children}</RealmIdentityContext.Provider>
51
49
) : (
52
50
props.fallback({loading: !error$.value, error: error$.value})
53
51
)
+1
-4
src/realm/client/service-connection-peer.ts
+1
-4
src/realm/client/service-connection-peer.ts
+1
-3
src/realm/client/service-connection.ts
+1
-3
src/realm/client/service-connection.ts
···
333
333
334
334
switch (parse.data.msg) {
335
335
case 'realm.rtc.signal': {
336
-
const jwt = await jwtPayload(protocol.realmRtcSignalPayloadSchema).parseAsync(
337
-
parse.data.dat.signed,
338
-
)
336
+
const jwt = await jwtPayload(protocol.realmRtcSignalPayloadSchema).parseAsync(parse.data.dat.signed)
339
337
340
338
// backwards from our perspective
341
339
const remoteid = parse.data.dat.localid
+1
-5
src/realm/protocol/index.ts
+1
-5
src/realm/protocol/index.ts
···
7
7
export * from './brands'
8
8
export * from './messages'
9
9
10
-
export function makeError(
11
-
error: ProtocolError,
12
-
detail: string,
13
-
seq?: number,
14
-
): z.infer<typeof errorMessageSchema> {
10
+
export function makeError(error: ProtocolError, detail: string, seq?: number): z.infer<typeof errorMessageSchema> {
15
11
return {
16
12
typ: 'err',
17
13
msg: error.message,
+3
-14
src/realm/protocol/messages.ts
+3
-14
src/realm/protocol/messages.ts
···
4
4
import {IdentBrand} from './brands'
5
5
import {deviceCapsSchema, deviceInfoSchema} from './device'
6
6
import {LogicalClock} from './logical-clock'
7
-
import {
8
-
makeEmptyRequestSchema,
9
-
makeEventSchema,
10
-
makeRequestSchema,
11
-
makeResponseSchema,
12
-
} from './schema'
7
+
import {makeEmptyRequestSchema, makeEventSchema, makeRequestSchema, makeResponseSchema} from './schema'
13
8
14
9
export const serverPeerIdSchema = z.literal('server')
15
10
export type ServerPeerId = z.infer<typeof serverPeerIdSchema>
···
18
13
19
14
/// preauth
20
15
21
-
export const preauthRegisterReqSchema = makeRequestSchema(
22
-
'preauth.register',
23
-
z.object({pubkey: jwkSchema}),
24
-
)
16
+
export const preauthRegisterReqSchema = makeRequestSchema('preauth.register', z.object({pubkey: jwkSchema}))
25
17
26
18
export const preauthAuthnReqSchema = makeEmptyRequestSchema('preauth.authn')
27
19
···
70
62
}),
71
63
)
72
64
73
-
export const realmRtcPongResponseSchema = makeResponseSchema(
74
-
'realm.rtc.pong',
75
-
z.object({peerClocks: peerClocksSchema}),
76
-
)
65
+
export const realmRtcPongResponseSchema = makeResponseSchema('realm.rtc.pong', z.object({peerClocks: peerClocksSchema}))
77
66
78
67
export const realmRtcAnnounceRequestSchema = makeRequestSchema(
79
68
'realm.rtc.announce',
+4
-18
src/realm/server/handler-preauth.ts
+4
-18
src/realm/server/handler-preauth.ts
···
5
5
import {jwtPayload, jwtSchema, JWTToken, verifyJwtToken} from '#common/crypto/jwts'
6
6
import {normalizeError, ProtocolError} from '#common/errors'
7
7
import {takeSocket} from '#common/socket'
8
-
import {
9
-
IdentBrand,
10
-
IdentID,
11
-
preauthReqSchema,
12
-
PreauthResponse,
13
-
RealmBrand,
14
-
RealmID,
15
-
} from '#realm/protocol/index'
8
+
import {IdentBrand, IdentID, preauthReqSchema, PreauthResponse, RealmBrand, RealmID} from '#realm/protocol/index'
16
9
17
10
import * as realms from './state'
18
11
···
21
14
* - if the realm does not exist (by realm id), we create a new one, and add the identity (success).
22
15
* - if the realm /does/ exist, we verify the message is a signed JWT our already registered pubkey.
23
16
*/
24
-
export async function preauthHandler(
25
-
ws: WebSocket,
26
-
signal?: AbortSignal,
27
-
): Promise<realms.AuthenticatedIdentity> {
17
+
export async function preauthHandler(ws: WebSocket, signal?: AbortSignal): Promise<realms.AuthenticatedIdentity> {
28
18
const timeout = timeoutSignal(3000)
29
19
const combinedSignal = combineSignals(signal, timeout.signal)
30
20
···
75
65
const realm = realmMap.require(realmid)
76
66
77
67
if (!invitation.claims.jti) throw new Error('invitation requires nonce!')
78
-
if (!(await realms.validateNonce(realmid, invitation.claims.jti)))
79
-
throw new Error('invitation already used!')
68
+
if (!(await realms.validateNonce(realmid, invitation.claims.jti))) throw new Error('invitation already used!')
80
69
81
70
const inviterid = IdentBrand.parse(invitation.claims.iss)
82
71
const inviterkey = realm.storage.identities.require(inviterid)
···
107
96
}
108
97
}
109
98
110
-
async function preauthResponse(
111
-
auth: realms.AuthenticatedIdentity,
112
-
seq?: number,
113
-
): Promise<PreauthResponse> {
99
+
async function preauthResponse(auth: realms.AuthenticatedIdentity, seq?: number): Promise<PreauthResponse> {
114
100
const peers: Array<IdentID | 'server'> = Array.from(auth.realm.sockets.keys())
115
101
peers.unshift('server')
116
102
+4
-16
src/realm/server/handler-realm.ts
+4
-16
src/realm/server/handler-realm.ts
···
24
24
* ance we've retrieved authentication details, we go into the main realm loop.
25
25
* read messages as they come in and dispatch actions.
26
26
*/
27
-
export async function realmHandler(
28
-
ws: WebSocket,
29
-
auth: realm.AuthenticatedIdentity,
30
-
signal?: AbortSignal,
31
-
) {
27
+
export async function realmHandler(ws: WebSocket, auth: realm.AuthenticatedIdentity, signal?: AbortSignal) {
32
28
realmBroadcast(auth, await buildRtcPeerJoined(auth))
33
29
34
30
try {
···
77
73
}
78
74
}
79
75
80
-
async function buildRtcPeerJoined(
81
-
auth: realm.AuthenticatedIdentity,
82
-
): Promise<protocol.RealmRtcPeerJoinedEvent> {
76
+
async function buildRtcPeerJoined(auth: realm.AuthenticatedIdentity): Promise<protocol.RealmRtcPeerJoinedEvent> {
83
77
return {
84
78
typ: 'evt',
85
79
msg: 'realm.rtc.peer-joined',
···
112
106
recipients: protocol.IdentID[] | boolean = false,
113
107
) {
114
108
const echo = recipients === true || Array.isArray(recipients)
115
-
const recips = Array.isArray(recipients)
116
-
? recipients
117
-
: Array.from(auth.realm.storage.identities.keys())
109
+
const recips = Array.isArray(recipients) ? recipients : Array.from(auth.realm.storage.identities.keys())
118
110
const json = JSON.stringify(payload)
119
111
120
112
for (const recip of recips) {
···
129
121
}
130
122
}
131
123
132
-
async function socketPeerPing(
133
-
ws: WebSocket,
134
-
auth: realm.AuthenticatedIdentity,
135
-
ping: protocol.RealmRtcPingRequest,
136
-
) {
124
+
async function socketPeerPing(ws: WebSocket, auth: realm.AuthenticatedIdentity, ping: protocol.RealmRtcPingRequest) {
137
125
console.debug('ping from', auth.identid, ping.dat)
138
126
139
127
const peerClocks = await auth.realm.storage.buildSyncState()
+2
-11
src/realm/server/state-storage.ts
+2
-11
src/realm/server/state-storage.ts
···
88
88
return new RealmStorage(realmid, identities, db, metapath)
89
89
}
90
90
91
-
static async ensure(
92
-
realmid: RealmID,
93
-
registrantid: IdentID,
94
-
registrantkey: CryptoKey,
95
-
): Promise<RealmStorage> {
91
+
static async ensure(realmid: RealmID, registrantid: IdentID, registrantkey: CryptoKey): Promise<RealmStorage> {
96
92
try {
97
93
return await this.open(realmid)
98
94
} catch {
···
118
114
#db: Level
119
115
#metadataPath: string
120
116
121
-
private constructor(
122
-
realmid: RealmID,
123
-
identities: StrictMap<IdentID, CryptoKey>,
124
-
db: Level,
125
-
metadataPath: string,
126
-
) {
117
+
private constructor(realmid: RealmID, identities: StrictMap<IdentID, CryptoKey>, db: Level, metadataPath: string) {
127
118
this.realmid = realmid
128
119
this.identities = identities
129
120
+1
-5
src/skypod/actions.ts
+1
-5
src/skypod/actions.ts
···
18
18
export type ActionOpts = z.infer<typeof actionOptionsSchema>
19
19
export type ActionMap = {[K in Action as K['msg']]: K}
20
20
21
-
export function makeAction<N extends Action['msg']>(
22
-
msg: N,
23
-
clk: LCTimestamp,
24
-
dat: ActionMap[N]['dat'],
25
-
): ActionMap[N] {
21
+
export function makeAction<N extends Action['msg']>(msg: N, clk: LCTimestamp, dat: ActionMap[N]['dat']): ActionMap[N] {
26
22
return {typ: 'act', clk, msg, dat} as ActionMap[N]
27
23
}