validate access rules

This commit is contained in:
boufni95 2024-09-20 20:08:03 +00:00
parent 9a1aff58d0
commit a2d502baa9
9 changed files with 161 additions and 42 deletions

View file

@ -800,6 +800,7 @@ The nostr server will send back a message response, and inside the body there wi
- __admin_token__: _string_
### FrequencyRule
- __amount__: _number_
- __interval__: _[IntervalType](#IntervalType)_
- __number_of_intervals__: _number_
@ -855,7 +856,6 @@ The nostr server will send back a message response, and inside the body there wi
- __token__: _string_
### LiveDebitRequest
- __amount__: _number_
- __debit__: _[LiveDebitRequest_debit](#LiveDebitRequest_debit)_
- __npub__: _string_

View file

@ -202,6 +202,7 @@ type EnrollAdminTokenRequest struct {
Admin_token string `json:"admin_token"`
}
type FrequencyRule struct {
Amount int64 `json:"amount"`
Interval IntervalType `json:"interval"`
Number_of_intervals int64 `json:"number_of_intervals"`
}
@ -257,7 +258,6 @@ type LinkNPubThroughTokenRequest struct {
Token string `json:"token"`
}
type LiveDebitRequest struct {
Amount int64 `json:"amount"`
Debit *LiveDebitRequest_debit `json:"debit"`
Npub string `json:"npub"`
}

View file

@ -1094,12 +1094,14 @@ export const EnrollAdminTokenRequestValidate = (o?: EnrollAdminTokenRequest, opt
}
export type FrequencyRule = {
amount: number
interval: IntervalType
number_of_intervals: number
}
export const FrequencyRuleOptionalFields: [] = []
export type FrequencyRuleOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
amount_CustomCheck?: (v: number) => boolean
interval_CustomCheck?: (v: IntervalType) => boolean
number_of_intervals_CustomCheck?: (v: number) => boolean
}
@ -1107,6 +1109,9 @@ export const FrequencyRuleValidate = (o?: FrequencyRule, opts: FrequencyRuleOpti
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.amount !== 'number') return new Error(`${path}.amount: is not a number`)
if (opts.amount_CustomCheck && !opts.amount_CustomCheck(o.amount)) return new Error(`${path}.amount: custom check failed`)
if (!enumCheckIntervalType(o.interval)) return new Error(`${path}.interval: is not a valid IntervalType`)
if (opts.interval_CustomCheck && !opts.interval_CustomCheck(o.interval)) return new Error(`${path}.interval: custom check failed`)
@ -1419,14 +1424,12 @@ export const LinkNPubThroughTokenRequestValidate = (o?: LinkNPubThroughTokenRequ
}
export type LiveDebitRequest = {
amount: number
debit: LiveDebitRequest_debit
npub: string
}
export const LiveDebitRequestOptionalFields: [] = []
export type LiveDebitRequestOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
amount_CustomCheck?: (v: number) => boolean
debit_Options?: LiveDebitRequest_debitOptions
npub_CustomCheck?: (v: string) => boolean
}
@ -1434,9 +1437,6 @@ export const LiveDebitRequestValidate = (o?: LiveDebitRequest, opts: LiveDebitRe
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.amount !== 'number') return new Error(`${path}.amount: is not a number`)
if (opts.amount_CustomCheck && !opts.amount_CustomCheck(o.amount)) return new Error(`${path}.amount: custom check failed`)
const debitErr = LiveDebitRequest_debitValidate(o.debit, opts.debit_Options, `${path}.debit`)
if (debitErr !== null) return debitErr

View file

@ -512,6 +512,7 @@ enum IntervalType {
message FrequencyRule {
int64 number_of_intervals = 1;
IntervalType interval = 2;
int64 amount = 3;
}
message DebitRule {
@ -523,7 +524,6 @@ message DebitRule {
message LiveDebitRequest {
string npub = 1;
int64 amount = 2;
oneof debit {
string invoice = 3;
FrequencyRule frequency = 4;

View file

@ -5,6 +5,9 @@ import Storage from '../storage/index.js'
import LND from "../lnd/lnd.js"
import { ERROR, getLogger } from "../helpers/logger.js";
import { DebitAccess, DebitAccessRules } from '../storage/entity/DebitAccess.js';
import paymentManager from './paymentManager.js';
import { Application } from '../storage/entity/Application.js';
import { ApplicationUser } from '../storage/entity/ApplicationUser.js';
export const expirationRuleName = 'expiration'
export const frequencyRuleName = 'frequency'
type RecurringDebitTimeUnit = 'day' | 'week' | 'month'
@ -16,8 +19,78 @@ const unitToIntervalType = (unit: RecurringDebitTimeUnit) => {
case 'month': return Types.IntervalType.MONTH
default: throw new Error("invalid unit")
}
}
const intervalTypeToUnit = (interval: Types.IntervalType): RecurringDebitTimeUnit => {
switch (interval) {
case Types.IntervalType.DAY: return 'day'
case Types.IntervalType.WEEK: return 'week'
case Types.IntervalType.MONTH: return 'month'
default: throw new Error("invalid interval")
}
}
const IntervalTypeToSeconds = (interval: Types.IntervalType) => {
switch (interval) {
case Types.IntervalType.DAY: return 24 * 60 * 60
case Types.IntervalType.WEEK: return 7 * 24 * 60 * 60
case Types.IntervalType.MONTH: return 30 * 24 * 60 * 60
default: throw new Error("invalid interval")
}
}
const debitRulesToDebitAccessRules = (rule: Types.DebitRule[]): DebitAccessRules | undefined => {
let rules: DebitAccessRules | undefined = undefined
rule.forEach(r => {
if (!rules) {
rules = {}
}
const { rule } = r
switch (rule.type) {
case Types.DebitRule_rule_type.EXPIRATION_RULE:
rules[expirationRuleName] = [rule.expiration_rule.expires_at_unix.toString()]
break
case Types.DebitRule_rule_type.FREQUENCY_RULE:
const intervals = rule.frequency_rule.number_of_intervals.toString()
const unit = intervalTypeToUnit(rule.frequency_rule.interval)
return { key: frequencyRuleName, val: [intervals, unit, rule.frequency_rule.amount.toString()] }
default:
throw new Error("invalid rule")
}
})
return rules
}
const debitAccessRulesToDebitRules = (rules: DebitAccessRules | null): Types.DebitRule[] => {
if (!rules) {
return []
}
return Object.entries(rules).map(([key, val]) => {
switch (key) {
case expirationRuleName:
return {
rule: {
type: Types.DebitRule_rule_type.EXPIRATION_RULE,
expiration_rule: {
expires_at_unix: +val[0]
}
}
}
case frequencyRuleName:
return {
rule: {
type: Types.DebitRule_rule_type.FREQUENCY_RULE,
frequency_rule: {
number_of_intervals: +val[0],
interval: unitToIntervalType(val[1] as RecurringDebitTimeUnit),
amount: +val[2]
}
}
}
default:
throw new Error("invalid rule")
}
})
}
export type NdebitData = { pointer?: string, amount_sats: number } & (RecurringDebit | { bolt11: string })
export type NdebitSuccess = { res: 'ok' }
export type NdebitSuccessPayment = { res: 'ok', preimage: string }
@ -31,14 +104,15 @@ const nip68errs = {
6: "Invalid Request",
}
type HandleNdebitRes = { status: 'fail', debitRes: NdebitFailure }
| { status: 'invoicePaid', op: Types.UserOperation, appUserId: string, debitRes: NdebitSuccessPayment }
| { status: 'authRequired', liveDebitReq: Types.LiveDebitRequest, appUserId: string }
| { status: 'invoicePaid', op: Types.UserOperation, app: Application, appUser: ApplicationUser, debitRes: NdebitSuccessPayment }
| { status: 'authRequired', liveDebitReq: Types.LiveDebitRequest, app: Application, appUser: ApplicationUser }
| { status: 'authOk', debitRes: NdebitSuccess }
export class DebitManager {
applicationManager: ApplicationManager
storage: Storage
lnd: LND
logger = getLogger({ component: 'DebitManager' })
@ -49,12 +123,16 @@ export class DebitManager {
}
AuthorizeDebit = async (ctx: Types.UserContext, req: Types.DebitAuthorizationRequest): Promise<Types.DebitAuthorization> => {
const access = await this.storage.debitStorage.AddDebitAccess(ctx.app_user_id, req.authorize_npub)
const access = await this.storage.debitStorage.AddDebitAccess(ctx.app_user_id, {
authorize: true,
npub: req.authorize_npub,
rules: debitRulesToDebitAccessRules(req.rules)
})
return {
debit_id: access.serial_id.toString(),
npub: req.authorize_npub,
authorized: true,
rules: []
rules: req.rules
}
}
@ -64,7 +142,7 @@ export class DebitManager {
debit_id: access.serial_id.toString(),
authorized: access.authorized,
npub: access.npub,
rules: []
rules: debitAccessRulesToDebitRules(access.rules)
}))
return { debits }
}
@ -92,6 +170,8 @@ export class DebitManager {
return { status: 'fail', debitRes: { res: 'GFY', error: nip68errs[1], code: 1 } }
}
const appUserId = pointer
const app = await this.storage.applicationStorage.GetApplication(appId)
const appUser = await this.storage.applicationStorage.GetApplicationUser(app, appUserId)
const pointerFreq = pointerdata as RecurringDebit
if (pointerFreq.frequency) {
if (!amount_sats) {
@ -100,14 +180,14 @@ export class DebitManager {
const debitAccess = await this.storage.debitStorage.GetDebitAccess(appUserId, requestorPub)
if (!debitAccess) {
return {
status: 'authRequired', appUserId, liveDebitReq: {
amount: pointerdata.amount_sats,
status: 'authRequired', app, appUser, liveDebitReq: {
npub: requestorPub,
debit: {
type: Types.LiveDebitRequest_debit_type.FREQUENCY,
frequency: {
interval: unitToIntervalType(pointerFreq.frequency.unit),
number_of_intervals: pointerFreq.frequency.number,
amount: pointerdata.amount_sats,
}
}
}
@ -132,8 +212,7 @@ export class DebitManager {
const authorization = await this.storage.debitStorage.GetDebitAccess(appUserId, requestorPub)
if (!authorization) {
return {
status: 'authRequired', appUserId, liveDebitReq: {
amount: pointerdata.amount_sats,
status: 'authRequired', app, appUser, liveDebitReq: {
npub: requestorPub,
debit: {
type: Types.LiveDebitRequest_debit_type.INVOICE,
@ -145,33 +224,46 @@ export class DebitManager {
if (!authorization.authorized) {
return { status: 'fail', debitRes: { res: 'GFY', error: nip68errs[1], code: 1 } }
}
await this.validateAccessRules(authorization)
await this.validateAccessRules(authorization, app, appUser)
const payment = await this.applicationManager.PayAppUserInvoice(appId, { amount: 0, invoice: bolt11, user_identifier: appUserId })
await this.storage.debitStorage.IncrementDebitAccess(appUserId, requestorPub, payment.amount_paid + payment.service_fee + payment.network_fee)
const op = this.newPaymentOperation(payment, bolt11)
return { status: 'invoicePaid', op, appUserId, debitRes: { res: 'ok', preimage: payment.preimage } }
return { status: 'invoicePaid', op, app, appUser, debitRes: { res: 'ok', preimage: payment.preimage } }
}
validateAccessRules = async (access: DebitAccess): Promise<boolean> => {
validateAccessRules = async (access: DebitAccess, app: Application, appUser: ApplicationUser): Promise<boolean> => {
const { rules } = access
if (!rules) {
return true
}
if (rules[expirationRuleName]) {
const [expiration] = rules[expirationRuleName]
if (+expiration < Date.now()) {
await this.storage.debitStorage.RemoveDebitAccess(access.app_user_id, access.npub)
return false
// TODO: rules validation
/* if (rules[expirationRuleName]) {
}
}
if (rules[frequencyRuleName]) {
} */
const [number, unit, max] = rules[frequencyRuleName]
const intervalType = unitToIntervalType(unit as RecurringDebitTimeUnit)
const seconds = IntervalTypeToSeconds(intervalType) * (+number)
const sinceUnix = Math.floor(Date.now() / 1000) * seconds
const payments = await this.storage.paymentStorage.GetUserDebitPayments(appUser.user.user_id, sinceUnix, access.npub)
let total = 0
for (const payment of payments) {
total += payment.paid_amount
}
if (total > +max) {
return false
}
}
return true
}
newPaymentOperation = (payment: Types.PayInvoiceResponse, bolt11: string) => {
return {
amount: payment.amount_paid,
paidAtUnix: Date.now() / 1000,
paidAtUnix: Math.floor(Date.now() / 1000),
inbound: false,
type: Types.UserOperationType.OUTGOING_INVOICE,
identifier: bolt11,

View file

@ -328,8 +328,7 @@ export default class {
this.nostrSend({ type: 'app', appId: event.appId }, { type: 'event', event: e, encrypt: { toPub: event.pub } })
return
}
const app = await this.storage.applicationStorage.GetApplication(event.appId)
const appUser = await this.storage.applicationStorage.GetApplicationUser(app, res.appUserId)
const { appUser } = res
if (res.status === 'authRequired') {
const message: Types.LiveDebitRequest & { requestId: string, status: 'OK' } = { ...res.liveDebitReq, requestId: "GetLiveDebitRequests", status: 'OK' }
if (appUser.nostr_public_key) {// TODO - fix before support for http streams

View file

@ -2,6 +2,11 @@ import { DataSource, EntityManager } from "typeorm"
import UserStorage from './userStorage.js';
import TransactionsQueue from "./transactionsQueue.js";
import { DebitAccess, DebitAccessRules } from "./entity/DebitAccess.js";
type AccessToAdd = {
npub: string
rules?: DebitAccessRules
authorize: boolean
}
export default class {
DB: DataSource | EntityManager
txQueue: TransactionsQueue
@ -10,11 +15,12 @@ export default class {
this.txQueue = txQueue
}
async AddDebitAccess(appUserId: string, pubToAuthorize: string, authorize = true, entityManager = this.DB) {
async AddDebitAccess(appUserId: string, access: AccessToAdd, entityManager = this.DB) {
const entry = entityManager.getRepository(DebitAccess).create({
app_user_id: appUserId,
npub: pubToAuthorize,
authorized: authorize,
npub: access.npub,
authorized: access.authorize,
rules: access.rules,
})
return this.txQueue.PushToQueue<DebitAccess>({ exec: async db => db.getRepository(DebitAccess).save(entry), dbTx: false })
}
@ -41,7 +47,7 @@ export default class {
async DenyDebitAccess(appUserId: string, pub: string) {
const access = await this.GetDebitAccess(appUserId, pub)
if (!access) {
await this.AddDebitAccess(appUserId, pub, false)
await this.AddDebitAccess(appUserId, { npub: pub, authorize: false })
}
await this.UpdateDebitAccess(appUserId, pub, false)
}

View file

@ -44,6 +44,9 @@ export class UserInvoicePayment {
})
paymentIndex: number
@Column({ nullable: true })
debit_to_pub: string
@CreateDateColumn()
created_at: Date

View file

@ -1,5 +1,5 @@
import crypto from 'crypto';
import { Between, DataSource, EntityManager, FindOperator, IsNull, LessThanOrEqual, MoreThan, MoreThanOrEqual } from "typeorm"
import { Between, DataSource, EntityManager, FindOperator, IsNull, LessThanOrEqual, MoreThan, MoreThanOrEqual, Not } from "typeorm"
import { User } from './entity/User.js';
import { UserTransactionPayment } from './entity/UserTransactionPayment.js';
import { EphemeralKeyType, UserEphemeralKey } from './entity/UserEphemeralKey.js';
@ -215,6 +215,25 @@ export default class {
})
}
GetUserDebitPayments(userId: string, sinceUnix: number, debitToNpub: string, entityManager = this.DB): Promise<UserInvoicePayment[]> {
const pending = {
user: { user_id: userId },
debit_to_pub: debitToNpub,
paid_at_unix: 0,
}
const paid = {
user: { user_id: userId },
debit_to_pub: debitToNpub,
paid_at_unix: MoreThan(sinceUnix),
}
return entityManager.getRepository(UserInvoicePayment).find({
where: [pending, paid],
order: {
paid_at_unix: 'DESC'
}
})
}
async AddUserTransactionPayment(userId: string, address: string, txHash: string, txOutput: number, amount: number, chainFees: number, serviceFees: number, internal: boolean, height: number, linkedApplication: Application): Promise<UserTransactionPayment> {
const newTx = this.DB.getRepository(UserTransactionPayment).create({
user: await this.userStorage.GetUser(userId),