add alerter endpoints

This commit is contained in:
boufni95 2025-05-14 18:59:00 +00:00
parent 74e6d5b3ca
commit ed6036ce1e
20 changed files with 414 additions and 11 deletions

View file

@ -158,6 +158,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [GetPaymentStateRequest](#GetPaymentStateRequest)
- output: [PaymentState](#PaymentState)
- GetProvidersDisruption
- auth type: __Metrics__
- This methods has an __empty__ __request__ body
- output: [ProvidersDisruption](#ProvidersDisruption)
- GetSeed
- auth type: __Admin__
- This methods has an __empty__ __request__ body
@ -250,6 +255,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [PayInvoiceRequest](#PayInvoiceRequest)
- output: [PayInvoiceResponse](#PayInvoiceResponse)
- PingSubProcesses
- auth type: __Metrics__
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- ResetDebit
- auth type: __User__
- input: [DebitOperation](#DebitOperation)
@ -617,6 +627,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [GetPaymentStateRequest](#GetPaymentStateRequest)
- output: [PaymentState](#PaymentState)
- GetProvidersDisruption
- auth type: __Metrics__
- http method: __post__
- http route: __/api/metrics/providers/disruption__
- This methods has an __empty__ __request__ body
- output: [ProvidersDisruption](#ProvidersDisruption)
- GetSeed
- auth type: __Admin__
- http method: __get__
@ -790,6 +807,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [PayInvoiceRequest](#PayInvoiceRequest)
- output: [PayInvoiceResponse](#PayInvoiceResponse)
- PingSubProcesses
- auth type: __Metrics__
- http method: __post__
- http route: __/api/metrics/ping__
- This methods has an __empty__ __request__ body
- This methods has an __empty__ __response__ body
- RequestNPubLinkingToken
- auth type: __App__
- http method: __post__
@ -1352,6 +1376,14 @@ The nostr server will send back a message response, and inside the body there wi
- __noffer__: _string_
- __price_sats__: _number_
### ProviderDisruption
- __provider_pubkey__: _string_
- __provider_type__: _string_
- __since_unix__: _number_
### ProvidersDisruption
- __disruptions__: ARRAY of: _[ProviderDisruption](#ProviderDisruption)_
### RelaysMigration
- __relays__: ARRAY of: _string_

View file

@ -93,6 +93,7 @@ type Client struct {
GetMigrationUpdate func() (*MigrationUpdate, error)
GetNPubLinkingState func(req GetNPubLinking) (*NPubLinking, error)
GetPaymentState func(req GetPaymentStateRequest) (*PaymentState, error)
GetProvidersDisruption func() (*ProvidersDisruption, error)
GetSeed func() (*LndSeed, error)
GetSingleBundleMetrics func(req SingleMetricReq) (*BundleData, error)
GetSingleUsageMetrics func(req SingleMetricReq) (*UsageMetricTlv, error)
@ -116,6 +117,7 @@ type Client struct {
PayAddress func(req PayAddressRequest) (*PayAddressResponse, error)
PayAppUserInvoice func(req PayAppUserInvoiceRequest) (*PayInvoiceResponse, error)
PayInvoice func(req PayInvoiceRequest) (*PayInvoiceResponse, error)
PingSubProcesses func() error
RequestNPubLinkingToken func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error)
ResetDebit func(req DebitOperation) error
ResetMetricsStorages func() error
@ -1096,6 +1098,32 @@ func NewClient(params ClientParams) *Client {
}
return &res, nil
},
GetProvidersDisruption: func() (*ProvidersDisruption, error) {
auth, err := params.RetrieveMetricsAuth()
if err != nil {
return nil, err
}
finalRoute := "/api/metrics/providers/disruption"
body := []byte{}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return nil, err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return nil, err
}
if result.Status == "ERROR" {
return nil, fmt.Errorf(result.Reason)
}
res := ProvidersDisruption{}
err = json.Unmarshal(resBody, &res)
if err != nil {
return nil, err
}
return &res, nil
},
GetSeed: func() (*LndSeed, error) {
auth, err := params.RetrieveAdminAuth()
if err != nil {
@ -1726,6 +1754,27 @@ func NewClient(params ClientParams) *Client {
}
return &res, nil
},
PingSubProcesses: func() error {
auth, err := params.RetrieveMetricsAuth()
if err != nil {
return err
}
finalRoute := "/api/metrics/ping"
body := []byte{}
resBody, err := doPostRequest(params.BaseURL+finalRoute, body, auth)
if err != nil {
return err
}
result := ResultError{}
err = json.Unmarshal(resBody, &result)
if err != nil {
return err
}
if result.Status == "ERROR" {
return fmt.Errorf(result.Reason)
}
return nil
},
RequestNPubLinkingToken: func(req RequestNPubLinkingTokenRequest) (*RequestNPubLinkingTokenResponse, error) {
auth, err := params.RetrieveAppAuth()
if err != nil {

View file

@ -527,6 +527,14 @@ type Product struct {
Noffer string `json:"noffer"`
Price_sats int64 `json:"price_sats"`
}
type ProviderDisruption struct {
Provider_pubkey string `json:"provider_pubkey"`
Provider_type string `json:"provider_type"`
Since_unix int64 `json:"since_unix"`
}
type ProvidersDisruption struct {
Disruptions []ProviderDisruption `json:"disruptions"`
}
type RelaysMigration struct {
Relays []string `json:"relays"`
}

View file

@ -1138,6 +1138,25 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetProvidersDisruption) throw new Error('method: GetProvidersDisruption is not implemented')
app.post('/api/metrics/providers/disruption', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetProvidersDisruption', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.GetProvidersDisruption) throw new Error('method: GetProvidersDisruption is not implemented')
const authContext = await opts.MetricsAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
stats.validate = stats.guard
const query = req.query
const params = req.params
const response = await methods.GetProvidersDisruption({rpcName:'GetProvidersDisruption', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.GetSeed) throw new Error('method: GetSeed is not implemented')
app.get('/api/admin/seed', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetSeed', batch: false, nostr: false, batchSize: 0}
@ -1617,6 +1636,25 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.PingSubProcesses) throw new Error('method: PingSubProcesses is not implemented')
app.post('/api/metrics/ping', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'PingSubProcesses', batch: false, nostr: false, batchSize: 0}
const stats: Types.RequestStats = { startMs:req.startTimeMs || 0, start:req.startTime || 0n, parse: process.hrtime.bigint(), guard: 0n, validate: 0n, handle: 0n }
let authCtx: Types.AuthContext = {}
try {
if (!methods.PingSubProcesses) throw new Error('method: PingSubProcesses is not implemented')
const authContext = await opts.MetricsAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
stats.validate = stats.guard
const query = req.query
const params = req.params
await methods.PingSubProcesses({rpcName:'PingSubProcesses', ctx:authContext })
stats.handle = process.hrtime.bigint()
res.json({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
} catch (ex) { const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
})
if (!opts.allowNotImplementedMethods && !methods.RequestNPubLinkingToken) throw new Error('method: RequestNPubLinkingToken is not implemented')
app.post('/api/app/user/npub/token', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'RequestNPubLinkingToken', batch: false, nostr: false, batchSize: 0}

View file

@ -507,6 +507,20 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetProvidersDisruption: async (): Promise<ResultError | ({ status: 'OK' }& Types.ProvidersDisruption)> => {
const auth = await params.retrieveMetricsAuth()
if (auth === null) throw new Error('retrieveMetricsAuth() returned null')
let finalRoute = '/api/metrics/providers/disruption'
const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.ProvidersDisruptionValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetSeed: async (): Promise<ResultError | ({ status: 'OK' }& Types.LndSeed)> => {
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
@ -827,6 +841,17 @@ export default (params: ClientParams) => ({
}
return { status: 'ERROR', reason: 'invalid response' }
},
PingSubProcesses: async (): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveMetricsAuth()
if (auth === null) throw new Error('retrieveMetricsAuth() returned null')
let finalRoute = '/api/metrics/ping'
const { data } = await axios.post(params.baseUrl + finalRoute, {}, { headers: { 'authorization': auth } })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
RequestNPubLinkingToken: async (request: Types.RequestNPubLinkingTokenRequest): Promise<ResultError | ({ status: 'OK' }& Types.RequestNPubLinkingTokenResponse)> => {
const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null')

View file

@ -422,6 +422,20 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetProvidersDisruption: async (): Promise<ResultError | ({ status: 'OK' }& Types.ProvidersDisruption)> => {
const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'GetProvidersDisruption',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
const result = data
if(!params.checkResult) return { status: 'OK', ...result }
const error = Types.ProvidersDisruptionValidate(result)
if (error === null) { return { status: 'OK', ...result } } else return { status: 'ERROR', reason: error.message }
}
return { status: 'ERROR', reason: 'invalid response' }
},
GetSeed: async (): Promise<ResultError | ({ status: 'OK' }& Types.LndSeed)> => {
const auth = await params.retrieveNostrAdminAuth()
if (auth === null) throw new Error('retrieveNostrAdminAuth() returned null')
@ -685,6 +699,17 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
}
return { status: 'ERROR', reason: 'invalid response' }
},
PingSubProcesses: async (): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')
const nostrRequest: NostrRequest = {}
const data = await send(params.pubDestination, {rpcName:'PingSubProcesses',authIdentifier:auth, ...nostrRequest })
if (data.status === 'ERROR' && typeof data.reason === 'string') return data
if (data.status === 'OK') {
return data
}
return { status: 'ERROR', reason: 'invalid response' }
},
ResetDebit: async (request: Types.DebitOperation): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')

View file

@ -812,6 +812,19 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'GetProvidersDisruption':
try {
if (!methods.GetProvidersDisruption) throw new Error('method: GetProvidersDisruption is not implemented')
const authContext = await opts.NostrMetricsAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
stats.validate = stats.guard
const response = await methods.GetProvidersDisruption({rpcName:'GetProvidersDisruption', ctx:authContext })
stats.handle = process.hrtime.bigint()
res({status: 'OK', ...response})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'GetSeed':
try {
if (!methods.GetSeed) throw new Error('method: GetSeed is not implemented')
@ -1085,6 +1098,19 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'PingSubProcesses':
try {
if (!methods.PingSubProcesses) throw new Error('method: PingSubProcesses is not implemented')
const authContext = await opts.NostrMetricsAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
stats.validate = stats.guard
await methods.PingSubProcesses({rpcName:'PingSubProcesses', ctx:authContext })
stats.handle = process.hrtime.bigint()
res({status: 'OK'})
opts.metricsCallback([{ ...info, ...stats, ...authContext }])
}catch(ex){ const e = ex as any; logErrorAndReturnResponse(e, e.message || e, res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback); if (opts.throwErrors) throw e }
break
case 'ResetDebit':
try {
if (!methods.ResetDebit) throw new Error('method: ResetDebit is not implemented')

View file

@ -28,8 +28,8 @@ export type MetricsContext = {
app_id: string
operator_id: string
}
export type MetricsMethodInputs = GetAppsMetrics_Input | GetBundleMetrics_Input | GetErrorStats_Input | GetLndMetrics_Input | GetSingleBundleMetrics_Input | GetSingleUsageMetrics_Input | GetUsageMetrics_Input | ResetMetricsStorages_Input | SubmitWebRtcMessage_Input | ZipMetricsStorages_Input
export type MetricsMethodOutputs = GetAppsMetrics_Output | GetBundleMetrics_Output | GetErrorStats_Output | GetLndMetrics_Output | GetSingleBundleMetrics_Output | GetSingleUsageMetrics_Output | GetUsageMetrics_Output | ResetMetricsStorages_Output | SubmitWebRtcMessage_Output | ZipMetricsStorages_Output
export type MetricsMethodInputs = GetAppsMetrics_Input | GetBundleMetrics_Input | GetErrorStats_Input | GetLndMetrics_Input | GetProvidersDisruption_Input | GetSingleBundleMetrics_Input | GetSingleUsageMetrics_Input | GetUsageMetrics_Input | PingSubProcesses_Input | ResetMetricsStorages_Input | SubmitWebRtcMessage_Input | ZipMetricsStorages_Input
export type MetricsMethodOutputs = GetAppsMetrics_Output | GetBundleMetrics_Output | GetErrorStats_Output | GetLndMetrics_Output | GetProvidersDisruption_Output | GetSingleBundleMetrics_Output | GetSingleUsageMetrics_Output | GetUsageMetrics_Output | PingSubProcesses_Output | ResetMetricsStorages_Output | SubmitWebRtcMessage_Output | ZipMetricsStorages_Output
export type UserContext = {
app_id: string
app_user_id: string
@ -162,6 +162,9 @@ export type GetNPubLinkingState_Output = ResultError | ({ status: 'OK' } & NPubL
export type GetPaymentState_Input = {rpcName:'GetPaymentState', req: GetPaymentStateRequest}
export type GetPaymentState_Output = ResultError | ({ status: 'OK' } & PaymentState)
export type GetProvidersDisruption_Input = {rpcName:'GetProvidersDisruption'}
export type GetProvidersDisruption_Output = ResultError | ({ status: 'OK' } & ProvidersDisruption)
export type GetSeed_Input = {rpcName:'GetSeed'}
export type GetSeed_Output = ResultError | ({ status: 'OK' } & LndSeed)
@ -247,6 +250,9 @@ export type PayAppUserInvoice_Output = ResultError | ({ status: 'OK' } & PayInvo
export type PayInvoice_Input = {rpcName:'PayInvoice', req: PayInvoiceRequest}
export type PayInvoice_Output = ResultError | ({ status: 'OK' } & PayInvoiceResponse)
export type PingSubProcesses_Input = {rpcName:'PingSubProcesses'}
export type PingSubProcesses_Output = ResultError | { status: 'OK' }
export type RequestNPubLinkingToken_Input = {rpcName:'RequestNPubLinkingToken', req: RequestNPubLinkingTokenRequest}
export type RequestNPubLinkingToken_Output = ResultError | ({ status: 'OK' } & RequestNPubLinkingTokenResponse)
@ -340,6 +346,7 @@ export type ServerMethods = {
GetMigrationUpdate?: (req: GetMigrationUpdate_Input & {ctx: UserContext }) => Promise<void>
GetNPubLinkingState?: (req: GetNPubLinkingState_Input & {ctx: AppContext }) => Promise<NPubLinking>
GetPaymentState?: (req: GetPaymentState_Input & {ctx: UserContext }) => Promise<PaymentState>
GetProvidersDisruption?: (req: GetProvidersDisruption_Input & {ctx: MetricsContext }) => Promise<ProvidersDisruption>
GetSeed?: (req: GetSeed_Input & {ctx: AdminContext }) => Promise<LndSeed>
GetSingleBundleMetrics?: (req: GetSingleBundleMetrics_Input & {ctx: MetricsContext }) => Promise<BundleData>
GetSingleUsageMetrics?: (req: GetSingleUsageMetrics_Input & {ctx: MetricsContext }) => Promise<UsageMetricTlv>
@ -363,6 +370,7 @@ export type ServerMethods = {
PayAddress?: (req: PayAddress_Input & {ctx: UserContext }) => Promise<PayAddressResponse>
PayAppUserInvoice?: (req: PayAppUserInvoice_Input & {ctx: AppContext }) => Promise<PayInvoiceResponse>
PayInvoice?: (req: PayInvoice_Input & {ctx: UserContext }) => Promise<PayInvoiceResponse>
PingSubProcesses?: (req: PingSubProcesses_Input & {ctx: MetricsContext }) => Promise<void>
RequestNPubLinkingToken?: (req: RequestNPubLinkingToken_Input & {ctx: AppContext }) => Promise<RequestNPubLinkingTokenResponse>
ResetDebit?: (req: ResetDebit_Input & {ctx: UserContext }) => Promise<void>
ResetMetricsStorages?: (req: ResetMetricsStorages_Input & {ctx: MetricsContext }) => Promise<void>
@ -3030,6 +3038,57 @@ export const ProductValidate = (o?: Product, opts: ProductOptions = {}, path: st
return null
}
export type ProviderDisruption = {
provider_pubkey: string
provider_type: string
since_unix: number
}
export const ProviderDisruptionOptionalFields: [] = []
export type ProviderDisruptionOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
provider_pubkey_CustomCheck?: (v: string) => boolean
provider_type_CustomCheck?: (v: string) => boolean
since_unix_CustomCheck?: (v: number) => boolean
}
export const ProviderDisruptionValidate = (o?: ProviderDisruption, opts: ProviderDisruptionOptions = {}, path: string = 'ProviderDisruption::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (typeof o.provider_pubkey !== 'string') return new Error(`${path}.provider_pubkey: is not a string`)
if (opts.provider_pubkey_CustomCheck && !opts.provider_pubkey_CustomCheck(o.provider_pubkey)) return new Error(`${path}.provider_pubkey: custom check failed`)
if (typeof o.provider_type !== 'string') return new Error(`${path}.provider_type: is not a string`)
if (opts.provider_type_CustomCheck && !opts.provider_type_CustomCheck(o.provider_type)) return new Error(`${path}.provider_type: custom check failed`)
if (typeof o.since_unix !== 'number') return new Error(`${path}.since_unix: is not a number`)
if (opts.since_unix_CustomCheck && !opts.since_unix_CustomCheck(o.since_unix)) return new Error(`${path}.since_unix: custom check failed`)
return null
}
export type ProvidersDisruption = {
disruptions: ProviderDisruption[]
}
export const ProvidersDisruptionOptionalFields: [] = []
export type ProvidersDisruptionOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
disruptions_ItemOptions?: ProviderDisruptionOptions
disruptions_CustomCheck?: (v: ProviderDisruption[]) => boolean
}
export const ProvidersDisruptionValidate = (o?: ProvidersDisruption, opts: ProvidersDisruptionOptions = {}, path: string = 'ProvidersDisruption::root.'): Error | null => {
if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message')
if (typeof o !== 'object' || o === null) return new Error(path + ': object is not an instance of an object or is null')
if (!Array.isArray(o.disruptions)) return new Error(`${path}.disruptions: is not an array`)
for (let index = 0; index < o.disruptions.length; index++) {
const disruptionsErr = ProviderDisruptionValidate(o.disruptions[index], opts.disruptions_ItemOptions, `${path}.disruptions[${index}]`)
if (disruptionsErr !== null) return disruptionsErr
}
if (opts.disruptions_CustomCheck && !opts.disruptions_CustomCheck(o.disruptions)) return new Error(`${path}.disruptions: custom check failed`)
return null
}
export type RelaysMigration = {
relays: string[]
}