validate access rules
This commit is contained in:
parent
9a1aff58d0
commit
a2d502baa9
9 changed files with 161 additions and 42 deletions
|
|
@ -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_
|
||||
|
||||
|
|
|
|||
|
|
@ -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,9 +258,8 @@ type LinkNPubThroughTokenRequest struct {
|
|||
Token string `json:"token"`
|
||||
}
|
||||
type LiveDebitRequest struct {
|
||||
Amount int64 `json:"amount"`
|
||||
Debit *LiveDebitRequest_debit `json:"debit"`
|
||||
Npub string `json:"npub"`
|
||||
Debit *LiveDebitRequest_debit `json:"debit"`
|
||||
Npub string `json:"npub"`
|
||||
}
|
||||
type LiveUserOperation struct {
|
||||
Operation *UserOperation `json:"operation"`
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
return false
|
||||
// TODO: rules validation
|
||||
/* if (rules[expirationRuleName]) {
|
||||
|
||||
}
|
||||
if (rules[frequencyRuleName]) {
|
||||
|
||||
} */
|
||||
if (rules[expirationRuleName]) {
|
||||
const [expiration] = rules[expirationRuleName]
|
||||
if (+expiration < Date.now()) {
|
||||
await this.storage.debitStorage.RemoveDebitAccess(access.app_user_id, access.npub)
|
||||
return false
|
||||
}
|
||||
}
|
||||
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,
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -44,6 +44,9 @@ export class UserInvoicePayment {
|
|||
})
|
||||
paymentIndex: number
|
||||
|
||||
@Column({ nullable: true })
|
||||
debit_to_pub: string
|
||||
|
||||
@CreateDateColumn()
|
||||
created_at: Date
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -154,9 +154,9 @@ export default class {
|
|||
})
|
||||
}
|
||||
|
||||
async AddPendingExternalPayment(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, networkFee: number }, linkedApplication: Application, liquidityProvider: string | undefined,dbTx:DataSource|EntityManager): Promise<UserInvoicePayment> {
|
||||
async AddPendingExternalPayment(userId: string, invoice: string, amounts: { payAmount: number, serviceFee: number, networkFee: number }, linkedApplication: Application, liquidityProvider: string | undefined, dbTx: DataSource | EntityManager): Promise<UserInvoicePayment> {
|
||||
const newPayment = dbTx.getRepository(UserInvoicePayment).create({
|
||||
user: await this.userStorage.GetUser(userId,dbTx),
|
||||
user: await this.userStorage.GetUser(userId, dbTx),
|
||||
paid_amount: amounts.payAmount,
|
||||
invoice,
|
||||
routing_fees: amounts.networkFee,
|
||||
|
|
@ -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),
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue