Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
189 changes: 81 additions & 108 deletions packages/auth/src/connection/Connection.ts

Large diffs are not rendered by default.

40 changes: 7 additions & 33 deletions packages/auth/src/connection/MessageQueue.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { EventEmitter } from '@herbcaudill/eventemitter42'
import { debug } from '@localfirst/shared'
import { debug, Logger } from '@localfirst/shared'

const log = debug.extend('message-queue')
/**
Expand All @@ -19,18 +19,16 @@ export class MessageQueue<T> extends EventEmitter<MessageQueueEvents<T>> {
#outbound: Record<number, NumberedMessage<T>> = {}
#nextOutbound = 0
readonly #sendMessage: (message: NumberedMessage<T>) => void
#sharedLogger: any | undefined
private readonly logger: Logger

constructor({ sendMessage, timeout = 1000, createLogger = undefined }: Options<T>) {
constructor({ sendMessage, timeout = 1000, extendableLogger = undefined }: Options<T>) {
super()
this.#sendMessage = (message: NumberedMessage<T>) => {
this.#nextOutbound = message.index + 1
sendMessage(message)
}
this.#timeout = timeout
if (createLogger != null) {
this.#sharedLogger = createLogger('message-queue')
}
this.logger = extendableLogger != null ? extendableLogger.extend('message-queue') : new Logger({ moduleName: 'auth:message-queue' })
}

/**
Expand Down Expand Up @@ -88,7 +86,7 @@ export class MessageQueue<T> extends EventEmitter<MessageQueueEvents<T>> {

#processOutbound() {
// send outbound messages in order
this.#LOG('debug', 'processOutbound')
this.logger.debug('processOutbound')
while (this.#outbound[this.#nextOutbound]) {
const message = this.#outbound[this.#nextOutbound]
this.#sendMessage(message)
Expand All @@ -99,7 +97,7 @@ export class MessageQueue<T> extends EventEmitter<MessageQueueEvents<T>> {
* Receives any messages that are pending in the inbound queue, and requests any missing messages.
*/
#processInbound() {
this.#LOG('debug', 'processInbound')
this.logger.debug('processInbound')
// emit received messages in order
while (this.#inbound[this.#nextInbound]) {
const message = this.#inbound[this.#nextInbound]
Expand All @@ -122,30 +120,6 @@ export class MessageQueue<T> extends EventEmitter<MessageQueueEvents<T>> {
}
}

#LOG(level: 'info' | 'warn' | 'error' | 'debug', message: string, ...params: any[]) {
if (this.#sharedLogger == null) {
log(message, params)
return
}

switch (level) {
case 'info':
this.#sharedLogger.info(message, ...params)
break
case 'warn':
this.#sharedLogger.warn(message, ...params)
break
case 'error':
this.#sharedLogger.error(message, ...params)
break
case 'debug':
this.#sharedLogger.debug(message, ...params)
break
default:
throw new Error(`Unknown log level ${level}`)
}
}

#truncateMessageForLogging(message: any): string {
return findAllByKeyAndReplace(
JSON.parse(JSON.stringify(message)),
Expand Down Expand Up @@ -207,7 +181,7 @@ type Options<T> = {
/** Time to wait (in ms) before requesting a missing message */
timeout?: number

createLogger?: (name: string) => any
extendableLogger?: Logger
username?: string
}

Expand Down
6 changes: 4 additions & 2 deletions packages/auth/src/connection/getDeviceUserFromGraph.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import type { Keyring, UserWithSecrets } from '@localfirst/crdx'
import { assert } from '@localfirst/shared'
import { assert, Logger } from '@localfirst/shared'
import { generateProof } from 'invitation/generateProof.js'
import { generateStarterKeys } from 'invitation/generateStarterKeys.js'
import { KeyType } from 'util/index.js'
Expand All @@ -17,14 +17,16 @@ export const getDeviceUserFromGraph = ({
serializedGraph,
teamKeyring,
invitationSeed,
logger,
}: {
serializedGraph: Uint8Array
teamKeyring: Keyring
invitationSeed: string
logger: Logger
}): UserWithSecrets => {
const starterKeys = generateStarterKeys(invitationSeed)
const invitationId = generateProof(invitationSeed).id
const state = getTeamState(serializedGraph, teamKeyring)
const state = getTeamState(serializedGraph, teamKeyring, logger)

const { userId } = select.getInvitation(state, invitationId)
assert(userId) // since this is a device invitation the invitation info includes the userId that created it
Expand Down
49 changes: 14 additions & 35 deletions packages/auth/src/team/Team.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ import {
redactKeys,
} from '@localfirst/crdx'
import { randomKey, signatures, symmetric, type Base58 } from '@localfirst/crypto'
import { assert, debug } from '@localfirst/shared'
import { assert, debug, Logger } from '@localfirst/shared'
import * as identity from 'connection/identity.js'
import { type Challenge } from 'connection/types.js'
import * as devices from 'device/index.js'
Expand Down Expand Up @@ -65,9 +65,8 @@ export class Team extends EventEmitter<TeamEvents> {

private readonly store: Store<TeamState, TeamAction>
private readonly context: LocalUserContext
private readonly log: (o: any, ...args: any[]) => void
private readonly seed: string
private readonly sharedLogger: any | undefined
private logger: Logger

/**
* We can make a team instance either by creating a brand-new team, or restoring one from a stored graph.
Expand All @@ -77,7 +76,6 @@ export class Team extends EventEmitter<TeamEvents> {

// ignore coverage
this.seed = options.seed ?? randomKey()
this.sharedLogger = options.sharedLogger

if ('user' in options.context) {
this.context = options.context
Expand All @@ -93,12 +91,13 @@ export class Team extends EventEmitter<TeamEvents> {
}
const { device, user } = this.context

this.log = debug.extend(`auth:team:${this.userName}`)
this.LOG('debug', 'loading team')
const moduleName = `auth:team:${this.userName}`
this.logger = new Logger({ moduleName, sharedLogger: options.sharedLogger, extendSharedLogger: true })
this.logger.debug('loading team')

// Initialize a CRDX store for the team
if (isNewTeam(options)) {
this.LOG('debug', 'creating new team')
this.logger.debug('creating new team', options.teamName)
// Create a new team with the current user as founding member

assert(!this.isServer, `Servers can't create teams`)
Expand Down Expand Up @@ -129,13 +128,14 @@ export class Team extends EventEmitter<TeamEvents> {
initialState,
rootPayload,
keys: options.teamKeys,
})

logger: this.logger,
})
const metadata: TeamMetadata = options.metadata ?? {
selfAssignableRoles: []
}
this.dispatch({ type: 'SET_METADATA', payload: { metadata }}, options.teamKeys)
} else {
this.logger.debug('loading existing team')
// Rehydrate a team from an existing graph
// Create CRDX store
this.store = createStore({
Expand All @@ -145,10 +145,13 @@ export class Team extends EventEmitter<TeamEvents> {
initialState,
graph: maybeDeserialize(options.source, options.teamKeyring),
keys: options.teamKeyring,
logger: this.logger,
})
}

this.state = this.store.getState()
this.logger = this.logger.extend(this.id)
this.logger.debug('team loaded')

// Wire up event listeners
this.on('updated', () => {
Expand All @@ -157,30 +160,6 @@ export class Team extends EventEmitter<TeamEvents> {
})
}

private LOG = (level: 'info' | 'warn' | 'error' | 'debug', message: any, ...params: any[]) => {
if (this.sharedLogger == null) {
this.log(message, params)
return
}

switch (level) {
case 'info':
this.sharedLogger.info(message, ...params)
break
case 'warn':
this.sharedLogger.warn(message, ...params)
break
case 'error':
this.sharedLogger.error(message, ...params)
break
case 'debug':
this.sharedLogger.debug(message, ...params)
break
default:
throw new Error(`Unknown log level ${level}`)
}
}

/** ************** PUBLIC API */

public get graph() {
Expand Down Expand Up @@ -656,14 +635,14 @@ export class Team extends EventEmitter<TeamEvents> {
/** Once the new member has received the graph and can instantiate the team, they call this to add their device. */
public join = (teamKeyring: Keyring) => {
assert(!this.isServer, "Can't join as member on server")
this.LOG('debug', 'joining pre-existing team')
this.logger.debug('joining pre-existing team')

const { user, device } = this.context
const teamKeys = getLatestGeneration(teamKeyring)

const lockboxUserKeysForDevice = lockbox.create(user.keys, device.keys)

this.LOG('debug', 'Adding device on join')
this.logger.debug('Adding device on join')
this.dispatch(
{
type: 'ADD_DEVICE',
Expand Down
3 changes: 2 additions & 1 deletion packages/auth/src/team/createTeam.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,9 @@ import { TEAM_SCOPE } from './constants.js'
import { type LocalContext } from 'team/context.js'
import { Team } from 'team/Team.js'
import { TeamMetadata } from './index.js'
import { SharedLogger } from '@localfirst/shared'

export function createTeam(teamName: string, context: LocalContext, seed?: string, metadata?: TeamMetadata, sharedLogger?: any) {
export function createTeam(teamName: string, context: LocalContext, seed?: string, metadata?: TeamMetadata, sharedLogger?: SharedLogger) {
const teamKeys = createKeyset(TEAM_SCOPE, seed)
const defaultMetadata: TeamMetadata = {
selfAssignableRoles: []
Expand Down
7 changes: 6 additions & 1 deletion packages/auth/src/team/decryptTeamGraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
type TeamLink,
type TeamState,
} from './types.js'
import { Logger } from '@localfirst/shared'

/**
* Decrypts a graph.
Expand All @@ -31,6 +32,7 @@ export const decryptTeamGraph = ({
encryptedGraph,
teamKeys,
deviceKeys,
extendableLogger,
}: {
encryptedGraph: MaybePartlyDecryptedGraph<TeamAction, TeamContext>

Expand All @@ -45,7 +47,10 @@ export const decryptTeamGraph = ({
* rotated.
*/
deviceKeys: KeysetWithSecrets

extendableLogger?: Logger
}): TeamGraph => {
const logger = extendableLogger != null ? extendableLogger.extend('decryptTeamGraph') : new Logger({ moduleName: 'auth:decryptTeamGraph' })
const keyring = createKeyring(teamKeys)

const { encryptedLinks, childMap, root } = encryptedGraph
Expand All @@ -70,7 +75,7 @@ export const decryptTeamGraph = ({
}

// Reduce & see if there are new team keys
const newState = reducer(previousState, decryptedLink)
const newState = reducer(previousState, decryptedLink, logger)
let newKeys: KeysetWithSecrets | undefined
try {
newKeys = keys(newState, deviceKeys, TEAM_SCOPE)
Expand Down
5 changes: 3 additions & 2 deletions packages/auth/src/team/getTeamState.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import type { Keyring } from '@localfirst/crdx'
import { deserializeTeamGraph } from './serialize.js'
import { teamMachine } from './teamMachine.js'
import { Logger } from '@localfirst/shared'

export const getTeamState = (serializedGraph: Uint8Array, keyring: Keyring) => {
export const getTeamState = (serializedGraph: Uint8Array, keyring: Keyring, extendableLogger?: Logger) => {
const graph = deserializeTeamGraph(serializedGraph, keyring)
return teamMachine(graph)
return teamMachine(graph, extendableLogger)
}
3 changes: 2 additions & 1 deletion packages/auth/src/team/load.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ import { type Keyring, type KeysetWithSecrets, createKeyring } from '@localfirst
import { type TeamGraph } from './types.js'
import { type LocalContext } from 'team/context.js'
import { Team } from 'team/Team.js'
import { SharedLogger } from '@localfirst/shared'

export const load = (
source: Uint8Array | TeamGraph,
context: LocalContext,
teamKeys: KeysetWithSecrets | Keyring,
sharedLogger?: any
sharedLogger?: SharedLogger
) => {
const teamKeyring = createKeyring(teamKeys)
return new Team({ source, context, teamKeyring, sharedLogger })
Expand Down
7 changes: 5 additions & 2 deletions packages/auth/src/team/reducer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
} from './types.js'
import { validate } from './validate.js'
import { setMetadata } from './transforms/setMetadata.js'
import { Logger } from '@localfirst/shared'

/**
* Each link has a `type` and a `payload`, just like a Redux action. So we can derive a `TeamState`
Expand All @@ -47,19 +48,21 @@ import { setMetadata } from './transforms/setMetadata.js'
* @param state The team state as of the previous link in the signature chain.
* @param link The current link being processed.
*/
export const reducer: Reducer<TeamState, TeamAction, TeamContext> = (state, link) => {
export const reducer: Reducer<TeamState, TeamAction, TeamContext> = (state, link, extendableLogger) => {
const logger = extendableLogger != null ? extendableLogger.extend('reducer') : new Logger({ moduleName: 'auth:reducer' })
// Invalid links are marked to be discarded by the MembershipResolver due to conflicting
// concurrent actions. In most cases we just ignore these links and they don't affect state at
// all; but in some cases we need to clean up, for example when someone's admission is reversed
// but they already joined and had access to the chain.
if (link.isInvalid) {
logger.warn('Link is invalid', link)
return invalidLinkReducer(state, link)
}

state = clone(state)

// Make sure this link can be applied to the previous state & doesn't put us in an invalid state
const validation = validate(state, link)
const validation = validate(state, link, logger)
if (!validation.isValid) {
throw validation.error
}
Expand Down
7 changes: 4 additions & 3 deletions packages/auth/src/team/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import type { Lockbox } from 'lockbox/index.js'
import type { PermissionsMap, Role } from 'role/index.js'
import type { Host, Server } from 'server/index.js'
import type { ValidationResult } from 'util/index.js'
import { Logger, SharedLogger } from '@localfirst/shared'

// ********* MEMBER

Expand Down Expand Up @@ -76,7 +77,7 @@ export type TeamOptions = NewOrExisting & {
context: LocalContext

/** Logging instance shared with main application */
sharedLogger?: any
sharedLogger?: SharedLogger
}

/** Type guard for NewTeamOptions vs ExistingTeamOptions */
Expand Down Expand Up @@ -330,11 +331,11 @@ export type InvitationMap = Record<string, InvitationState>

// ********* VALIDATION

export type TeamStateValidator = (previousState: TeamState, link: TeamLink) => ValidationResult
export type TeamStateValidator = (previousState: TeamState, link: TeamLink, extendableLogger: Logger) => ValidationResult

export type TeamStateValidatorSet = Record<string, TeamStateValidator>

export type ValidationArgs = [TeamState, TeamLink]
export type ValidationArgs = [TeamState, TeamLink, Logger | undefined]

// ********* CRYPTO

Expand Down
Loading