From 8005b2a3ff78ed7818020c77f2460838c5db9979 Mon Sep 17 00:00:00 2001 From: Mothana Date: Sun, 13 Jul 2025 21:17:21 +0400 Subject: [PATCH] rfc 6570 callback templates and callback bearer tokens --- datasource.js | 3 +- package-lock.json | 16 +++++ package.json | 1 + proto/autogenerated/client.md | 11 +-- proto/autogenerated/go/types.go | 26 +++---- proto/autogenerated/ts/types.ts | 56 ++++++++++----- proto/service/structs.proto | 12 ++-- src/services/main/applicationManager.ts | 3 +- src/services/main/index.ts | 68 ++++++++++++++++--- src/services/main/managementManager.ts | 12 +--- src/services/main/offerManager.ts | 39 +++++------ src/services/storage/entity/UserOffer.ts | 9 ++- .../storage/entity/UserReceivingInvoice.ts | 7 ++ .../1752425992291-invoice_callback_urls.ts | 36 ++++++++++ src/services/storage/migrations/runner.ts | 3 +- src/services/storage/paymentStorage.ts | 4 +- 16 files changed, 222 insertions(+), 84 deletions(-) create mode 100644 src/services/storage/migrations/1752425992291-invoice_callback_urls.ts diff --git a/datasource.js b/datasource.js index c2b68147..08db3ca7 100644 --- a/datasource.js +++ b/datasource.js @@ -31,13 +31,14 @@ import { DebitToPub1727105758354 } from './build/src/services/storage/migrations import { UserCbUrl1727112281043 } from './build/src/services/storage/migrations/1727112281043-user_cb_url.js' import { UserOffer1733502626042 } from './build/src/services/storage/migrations/1733502626042-user_offer.js' import { ManagementGrant1751307732346 } from './build/src/services/storage/migrations/1751307732346-management_grant.js' +import { InvoiceCallbackUrls1752425992291 } from './build/src/services/storage/migrations/1752425992291-invoice_callback_urls.js' export default new DataSource({ type: "sqlite", database: "db.sqlite", // logging: true, migrations: [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, - UserOffer1733502626042, ManagementGrant1751307732346], + UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291], entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant], // synchronize: true, diff --git a/package-lock.json b/package-lock.json index 1c9a2b8d..ca0f1e6e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -44,6 +44,7 @@ "ts-proto": "^1.131.2", "typeorm": "0.3.15", "typescript": "^5.5.4", + "uri-template": "^2.0.0", "uuid": "^8.3.2", "websocket": "^1.0.35", "websocket-polyfill": "^0.0.3", @@ -4744,6 +4745,12 @@ "node": "*" } }, + "node_modules/pct-encode": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/pct-encode/-/pct-encode-1.0.3.tgz", + "integrity": "sha512-+ojEvSHApoLWF2YYxwnOM4N9DPn5e5fG+j0YJ9drKNaYtrZYOq5M9ESOaBYqOHCXOAALODJJ4wkqHAXEuLpwMw==", + "license": "BSD-2-Clause" + }, "node_modules/performance-now": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz", @@ -6486,6 +6493,15 @@ "punycode": "^2.1.0" } }, + "node_modules/uri-template": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/uri-template/-/uri-template-2.0.0.tgz", + "integrity": "sha512-r/i44nPoo0ktEZDjx+hxp9PSjQuBBfsd6RgCRuuMqCP0FZEp+YE0SpihThI4UGc5ePqQEFsdyZc7UVlowp+LLw==", + "license": "MIT", + "dependencies": { + "pct-encode": "~1.0.0" + } + }, "node_modules/utf-8-validate": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/utf-8-validate/-/utf-8-validate-5.0.10.tgz", diff --git a/package.json b/package.json index 3ded2084..71560be9 100644 --- a/package.json +++ b/package.json @@ -62,6 +62,7 @@ "ts-proto": "^1.131.2", "typeorm": "0.3.15", "typescript": "^5.5.4", + "uri-template": "^2.0.0", "uuid": "^8.3.2", "websocket": "^1.0.35", "websocket-polyfill": "^0.0.3", diff --git a/proto/autogenerated/client.md b/proto/autogenerated/client.md index 0f871cd6..fff56015 100644 --- a/proto/autogenerated/client.md +++ b/proto/autogenerated/client.md @@ -1021,6 +1021,8 @@ The nostr server will send back a message response, and inside the body there wi - __payer_data__: _[PayerData](#PayerData)_ *this field is optional - __payer_identifier__: _string_ - __receiver_identifier__: _string_ + - __rejectUnauthorized__: _boolean_ *this field is optional + - __token__: _string_ *this field is optional ### AddAppUserRequest - __balance__: _number_ @@ -1383,12 +1385,16 @@ The nostr server will send back a message response, and inside the body there wi ### OfferConfig - __callback_url__: _string_ + - __createdAtUnix__: _number_ - __default_offer__: _boolean_ - - __expected_data__: MAP with key: _string_ and value: _[OfferDataType](#OfferDataType)_ - __label__: _string_ - __noffer__: _string_ - __offer_id__: _string_ + - __payer_data__: ARRAY of: _string_ - __price_sats__: _number_ + - __rejectUnauthorized__: _boolean_ + - __token__: _string_ + - __updatedAtUnix__: _number_ ### OfferId - __offer_id__: _string_ @@ -1631,9 +1637,6 @@ The nostr server will send back a message response, and inside the body there wi - __MONTH__ - __WEEK__ -### OfferDataType - - __DATA_STRING__ - ### OperationType - __CHAIN_OP__ - __INVOICE_OP__ diff --git a/proto/autogenerated/go/types.go b/proto/autogenerated/go/types.go index f718dccf..7e8daa6d 100644 --- a/proto/autogenerated/go/types.go +++ b/proto/autogenerated/go/types.go @@ -65,12 +65,6 @@ const ( WEEK IntervalType = "WEEK" ) -type OfferDataType string - -const ( - DATA_STRING OfferDataType = "DATA_STRING" -) - type OperationType string const ( @@ -112,6 +106,8 @@ type AddAppUserInvoiceRequest struct { Payer_data *PayerData `json:"payer_data"` Payer_identifier string `json:"payer_identifier"` Receiver_identifier string `json:"receiver_identifier"` + Rejectunauthorized bool `json:"rejectUnauthorized"` + Token string `json:"token"` } type AddAppUserRequest struct { Balance int64 `json:"balance"` @@ -473,13 +469,17 @@ type NewInvoiceResponse struct { Invoice string `json:"invoice"` } type OfferConfig struct { - Callback_url string `json:"callback_url"` - Default_offer bool `json:"default_offer"` - Expected_data map[string]OfferDataType `json:"expected_data"` - Label string `json:"label"` - Noffer string `json:"noffer"` - Offer_id string `json:"offer_id"` - Price_sats int64 `json:"price_sats"` + Callback_url string `json:"callback_url"` + Createdatunix int64 `json:"createdAtUnix"` + Default_offer bool `json:"default_offer"` + Label string `json:"label"` + Noffer string `json:"noffer"` + Offer_id string `json:"offer_id"` + Payer_data []string `json:"payer_data"` + Price_sats int64 `json:"price_sats"` + Rejectunauthorized bool `json:"rejectUnauthorized"` + Token string `json:"token"` + Updatedatunix int64 `json:"updatedAtUnix"` } type OfferId struct { Offer_id string `json:"offer_id"` diff --git a/proto/autogenerated/ts/types.ts b/proto/autogenerated/ts/types.ts index 06af2539..cb3f357f 100644 --- a/proto/autogenerated/ts/types.ts +++ b/proto/autogenerated/ts/types.ts @@ -429,13 +429,6 @@ export const enumCheckIntervalType = (e?: IntervalType): boolean => { for (const v in IntervalType) if (e === v) return true return false } -export enum OfferDataType { - DATA_STRING = 'DATA_STRING', -} -export const enumCheckOfferDataType = (e?: OfferDataType): boolean => { - for (const v in OfferDataType) if (e === v) return true - return false -} export enum OperationType { CHAIN_OP = 'CHAIN_OP', INVOICE_OP = 'INVOICE_OP', @@ -528,9 +521,11 @@ export type AddAppUserInvoiceRequest = { payer_data?: PayerData payer_identifier: string receiver_identifier: string + rejectUnauthorized?: boolean + token?: string } -export type AddAppUserInvoiceRequestOptionalField = 'offer_string' | 'payer_data' -export const AddAppUserInvoiceRequestOptionalFields: AddAppUserInvoiceRequestOptionalField[] = ['offer_string', 'payer_data'] +export type AddAppUserInvoiceRequestOptionalField = 'offer_string' | 'payer_data' | 'rejectUnauthorized' | 'token' +export const AddAppUserInvoiceRequestOptionalFields: AddAppUserInvoiceRequestOptionalField[] = ['offer_string', 'payer_data', 'rejectUnauthorized', 'token'] export type AddAppUserInvoiceRequestOptions = OptionsBaseMessage & { checkOptionalsAreSet?: AddAppUserInvoiceRequestOptionalField[] http_callback_url_CustomCheck?: (v: string) => boolean @@ -539,6 +534,8 @@ export type AddAppUserInvoiceRequestOptions = OptionsBaseMessage & { payer_data_Options?: PayerDataOptions payer_identifier_CustomCheck?: (v: string) => boolean receiver_identifier_CustomCheck?: (v: string) => boolean + rejectUnauthorized_CustomCheck?: (v?: boolean) => boolean + token_CustomCheck?: (v?: string) => boolean } export const AddAppUserInvoiceRequestValidate = (o?: AddAppUserInvoiceRequest, opts: AddAppUserInvoiceRequestOptions = {}, path: string = 'AddAppUserInvoiceRequest::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') @@ -566,6 +563,12 @@ export const AddAppUserInvoiceRequestValidate = (o?: AddAppUserInvoiceRequest, o if (typeof o.receiver_identifier !== 'string') return new Error(`${path}.receiver_identifier: is not a string`) if (opts.receiver_identifier_CustomCheck && !opts.receiver_identifier_CustomCheck(o.receiver_identifier)) return new Error(`${path}.receiver_identifier: custom check failed`) + if ((o.rejectUnauthorized || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('rejectUnauthorized')) && typeof o.rejectUnauthorized !== 'boolean') return new Error(`${path}.rejectUnauthorized: is not a boolean`) + if (opts.rejectUnauthorized_CustomCheck && !opts.rejectUnauthorized_CustomCheck(o.rejectUnauthorized)) return new Error(`${path}.rejectUnauthorized: custom check failed`) + + if ((o.token || opts.allOptionalsAreSet || opts.checkOptionalsAreSet?.includes('token')) && typeof o.token !== 'string') return new Error(`${path}.token: is not a string`) + if (opts.token_CustomCheck && !opts.token_CustomCheck(o.token)) return new Error(`${path}.token: custom check failed`) + return null } @@ -2754,23 +2757,31 @@ export const NewInvoiceResponseValidate = (o?: NewInvoiceResponse, opts: NewInvo export type OfferConfig = { callback_url: string + createdAtUnix: number default_offer: boolean - expected_data: Record label: string noffer: string offer_id: string + payer_data: string[] price_sats: number + rejectUnauthorized: boolean + token: string + updatedAtUnix: number } export const OfferConfigOptionalFields: [] = [] export type OfferConfigOptions = OptionsBaseMessage & { checkOptionalsAreSet?: [] callback_url_CustomCheck?: (v: string) => boolean + createdAtUnix_CustomCheck?: (v: number) => boolean default_offer_CustomCheck?: (v: boolean) => boolean - expected_data_CustomCheck?: (v: Record) => boolean label_CustomCheck?: (v: string) => boolean noffer_CustomCheck?: (v: string) => boolean offer_id_CustomCheck?: (v: string) => boolean + payer_data_CustomCheck?: (v: string[]) => boolean price_sats_CustomCheck?: (v: number) => boolean + rejectUnauthorized_CustomCheck?: (v: boolean) => boolean + token_CustomCheck?: (v: string) => boolean + updatedAtUnix_CustomCheck?: (v: number) => boolean } export const OfferConfigValidate = (o?: OfferConfig, opts: OfferConfigOptions = {}, path: string = 'OfferConfig::root.'): Error | null => { if (opts.checkOptionalsAreSet && opts.allOptionalsAreSet) return new Error(path + ': only one of checkOptionalsAreSet or allOptionalNonDefault can be set for each message') @@ -2779,14 +2790,12 @@ export const OfferConfigValidate = (o?: OfferConfig, opts: OfferConfigOptions = if (typeof o.callback_url !== 'string') return new Error(`${path}.callback_url: is not a string`) if (opts.callback_url_CustomCheck && !opts.callback_url_CustomCheck(o.callback_url)) return new Error(`${path}.callback_url: custom check failed`) + if (typeof o.createdAtUnix !== 'number') return new Error(`${path}.createdAtUnix: is not a number`) + if (opts.createdAtUnix_CustomCheck && !opts.createdAtUnix_CustomCheck(o.createdAtUnix)) return new Error(`${path}.createdAtUnix: custom check failed`) + if (typeof o.default_offer !== 'boolean') return new Error(`${path}.default_offer: is not a boolean`) if (opts.default_offer_CustomCheck && !opts.default_offer_CustomCheck(o.default_offer)) return new Error(`${path}.default_offer: custom check failed`) - if (typeof o.expected_data !== 'object' || o.expected_data === null) return new Error(`${path}.expected_data: is not an object or is null`) - for (const key in o.expected_data) { - if (!enumCheckOfferDataType(o.expected_data[key])) return new Error(`${path}.expected_data['${key}']: is not a OfferDataType`) - } - if (typeof o.label !== 'string') return new Error(`${path}.label: is not a string`) if (opts.label_CustomCheck && !opts.label_CustomCheck(o.label)) return new Error(`${path}.label: custom check failed`) @@ -2796,9 +2805,24 @@ export const OfferConfigValidate = (o?: OfferConfig, opts: OfferConfigOptions = if (typeof o.offer_id !== 'string') return new Error(`${path}.offer_id: is not a string`) if (opts.offer_id_CustomCheck && !opts.offer_id_CustomCheck(o.offer_id)) return new Error(`${path}.offer_id: custom check failed`) + if (!Array.isArray(o.payer_data)) return new Error(`${path}.payer_data: is not an array`) + for (let index = 0; index < o.payer_data.length; index++) { + if (typeof o.payer_data[index] !== 'string') return new Error(`${path}.payer_data[${index}]: is not a string`) + } + if (opts.payer_data_CustomCheck && !opts.payer_data_CustomCheck(o.payer_data)) return new Error(`${path}.payer_data: custom check failed`) + if (typeof o.price_sats !== 'number') return new Error(`${path}.price_sats: is not a number`) if (opts.price_sats_CustomCheck && !opts.price_sats_CustomCheck(o.price_sats)) return new Error(`${path}.price_sats: custom check failed`) + if (typeof o.rejectUnauthorized !== 'boolean') return new Error(`${path}.rejectUnauthorized: is not a boolean`) + if (opts.rejectUnauthorized_CustomCheck && !opts.rejectUnauthorized_CustomCheck(o.rejectUnauthorized)) return new Error(`${path}.rejectUnauthorized: custom check failed`) + + if (typeof o.token !== 'string') return new Error(`${path}.token: is not a string`) + if (opts.token_CustomCheck && !opts.token_CustomCheck(o.token)) return new Error(`${path}.token: custom check failed`) + + if (typeof o.updatedAtUnix !== 'number') return new Error(`${path}.updatedAtUnix: is not a number`) + if (opts.updatedAtUnix_CustomCheck && !opts.updatedAtUnix_CustomCheck(o.updatedAtUnix)) return new Error(`${path}.updatedAtUnix: custom check failed`) + return null } diff --git a/proto/service/structs.proto b/proto/service/structs.proto index 43bb75ad..2b976afe 100644 --- a/proto/service/structs.proto +++ b/proto/service/structs.proto @@ -377,6 +377,9 @@ message AddAppUserInvoiceRequest { NewInvoiceRequest invoice_req = 4; optional PayerData payer_data = 5; optional string offer_string = 6; + optional bool rejectUnauthorized = 7; + optional string token = 8; + } message GetAppUserRequest { @@ -753,9 +756,6 @@ message DebitResponse { } } -enum OfferDataType { - DATA_STRING = 0; -} message OfferId { string offer_id = 1; @@ -766,9 +766,13 @@ message OfferConfig { string label = 2; int64 price_sats = 3; string callback_url = 4; - map expected_data = 5; + repeated string payer_data = 5; string noffer = 6; bool default_offer = 7; + string token = 8; + bool rejectUnauthorized = 9; + int64 createdAtUnix = 10; + int64 updatedAtUnix = 11; } message UserOffers { diff --git a/src/services/main/applicationManager.ts b/src/services/main/applicationManager.ts index d216f71c..aa00b9b3 100644 --- a/src/services/main/applicationManager.ts +++ b/src/services/main/applicationManager.ts @@ -193,7 +193,8 @@ export default class { } const opts: InboundOptionals = { callbackUrl: cbUrl, expiry: defaultInvoiceExpiry, expectedPayer: payer.user, linkedApplication: app, zapInfo, - offerId: req.offer_string, payerData: req.payer_data?.data + offerId: req.offer_string, payerData: req.payer_data?.data, rejectUnauthorized: req.rejectUnauthorized, + token: req.token } const appUserInvoice = await this.paymentManager.NewInvoice(receiver.user.user_id, req.invoice_req, opts) return { diff --git a/src/services/main/index.ts b/src/services/main/index.ts index 15f0cf7c..f426c51d 100644 --- a/src/services/main/index.ts +++ b/src/services/main/index.ts @@ -24,8 +24,10 @@ import { Unlocker } from "./unlocker.js" import { defaultInvoiceExpiry } from "../storage/paymentStorage.js" import { DebitManager } from "./debitManager.js" import { OfferManager } from "./offerManager.js" +import { parse } from "uri-template" import webRTC from "../webRTC/index.js" import { ManagementManager } from "./managementManager.js" +import { Agent } from "https" type UserOperationsSub = { id: string @@ -265,7 +267,7 @@ export default class { if (fee > 0) { await this.storage.userStorage.IncrementUserBalance(userInvoice.linkedApplication.owner.user_id, fee, 'fees', tx) } - await this.triggerPaidCallback(log, userInvoice.callbackUrl, { invoice: paymentRequest, amount, other: userInvoice.payer_data }) + await this.triggerPaidCallback(log, userInvoice.callbackUrl, { invoice: paymentRequest, amount, payerData: userInvoice.payer_data, token: userInvoice.bearer_token, rejectUnauthorized: userInvoice.rejectUnauthorized }) const operationId = `${Types.UserOperationType.INCOMING_INVOICE}-${userInvoice.serial_id}` const op = { amount, paidAtUnix: Date.now() / 1000, inbound: true, type: Types.UserOperationType.INCOMING_INVOICE, identifier: userInvoice.invoice, operationId, network_fee: 0, service_fee: fee, confirmed: true, tx_hash: "", internal } this.sendOperationToNostr(userInvoice.linkedApplication, userInvoice.user.user_id, op) @@ -283,21 +285,67 @@ export default class { }) } - async triggerPaidCallback(log: PubLogger, url: string, { invoice, amount, other }: { invoice: string, amount: number, other?: Record }) { + async triggerPaidCallback(log: PubLogger, url: string, + { invoice, amount, payerData, token, rejectUnauthorized }: + { invoice: string, + amount: number, + payerData?: Record, + token?: string, + rejectUnauthorized?: boolean + } + ) { if (!url) { return } - let finalUrl = url.replace(`%[invoice]`, invoice).replace(`%[amount]`, amount.toString()) - if (other) { - for (const [key, value] of Object.entries(other)) { - finalUrl = finalUrl.replace(`%[${key}]`, value) - } + let finalUrl = ""; + const payerDataToExpand = { + amount, + invoice, + ...(payerData !== undefined ? payerData : {}) + }; + try { + const parsed = parse(url); + finalUrl = parsed.expand(payerDataToExpand) + } catch (err: any) { + log(ERROR, "error expanding callback url template for invoice", err?.message || ""); + return; + } + const symbol = finalUrl.includes('?') ? "&" : "?" + finalUrl = finalUrl + symbol + "ok=true" + + /* + * Construct URL to find protocol. + * If it's https we then use an agent + * with the passed rejectUnauthorized + * value. + * If it's http we don't use an agent. + * If it's neither we log error and + * return. + */ + let parsedUrl: URL | null = null; + let agent: Agent | undefined; + try { + parsedUrl = new URL(finalUrl); + } catch (err: any) { + log(ERROR, "error parsing callback url for invoice", err?.message || ""); + return; + } + if (parsedUrl.protocol === "https:") { + agent = new Agent({ + rejectUnauthorized + }) + } else if (parsedUrl.protocol === "http:") { + agent = undefined + } else { + log(ERROR, "callback url's protocol is neither http or https"); + return; + } + const headers = { + ...(token ? { Authorization: `Bearer ${token}` } : {}) } try { - const symbol = finalUrl.includes('?') ? "&" : "?" - finalUrl = finalUrl + symbol + "ok=true" log("sending paid callback to", finalUrl) - await fetch(finalUrl) + await fetch(finalUrl, { agent, headers }) } catch (err: any) { log(ERROR, "error sending paid callback for invoice", err.message || "") } diff --git a/src/services/main/managementManager.ts b/src/services/main/managementManager.ts index ce3f255e..6c643620 100644 --- a/src/services/main/managementManager.ts +++ b/src/services/main/managementManager.ts @@ -150,7 +150,7 @@ export class ManagementManager { label: offer.label, price_sats: offer.price_sats, callback_url: offer.callback_url, - payer_data: Object.keys(offer.expected_data || {}), + payer_data: offer.payer_data || [], noffer: nofferEncode(pointer), } } @@ -229,15 +229,11 @@ export class ManagementManager { if (validateResult.state !== 'success') { return validateResult } - const dataMap: Record = {} - nmanageReq.offer.fields.payer_data.forEach(data => { - dataMap[data] = Types.OfferDataType.DATA_STRING - }) const offer = await this.storage.offerStorage.AddUserOffer(appUserId, { label: nmanageReq.offer.fields.label, callback_url: nmanageReq.offer.fields.callback_url, price_sats: nmanageReq.offer.fields.price_sats, - expected_data: dataMap, + payer_data: nmanageReq.offer.fields.payer_data, management_pubkey: requestorPub, }) return { state: 'success', result: offer } @@ -288,18 +284,16 @@ export class ManagementManager { if (validateResult.state !== 'success') { return validateResult } - const dataMap: Record = {} for (const data of nmanageReq.offer.fields.payer_data || []) { if (typeof data !== 'string') { return { state: 'error', err: { res: 'GFY', code: 5, error: 'Invalid Field/Value', field: 'payer_data' } } } - dataMap[data] = Types.OfferDataType.DATA_STRING } await this.storage.offerStorage.UpdateUserOffer(offer.result.app_user_id, nmanageReq.offer.id, { label: nmanageReq.offer.fields.label, callback_url: nmanageReq.offer.fields.callback_url, price_sats: nmanageReq.offer.fields.price_sats, - expected_data: dataMap, + payer_data: nmanageReq.offer.fields.payer_data, }) const updatedOffer = await this.storage.offerStorage.GetOffer(nmanageReq.offer.id) if (!updatedOffer) { diff --git a/src/services/main/offerManager.ts b/src/services/main/offerManager.ts index 3aaae490..7de2470b 100644 --- a/src/services/main/offerManager.ts +++ b/src/services/main/offerManager.ts @@ -12,15 +12,6 @@ import { NofferData, OfferPriceType, nofferEncode } from '@shocknet/clink-sdk'; import { MainSettings } from "./settings.js"; const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay }: { pubkey: string, relay: string }): Types.OfferConfig => { - if (offer.expected_data) { - const keys = Object.keys(offer.expected_data) - for (const key of keys) { - const v = offer.expected_data[key] as Types.OfferDataType - if (!Types.OfferDataType[v]) { - offer.expected_data[key] = Types.OfferDataType.DATA_STRING - } - } - } const offerStr = offer.offer_id const priceType: OfferPriceType = offer.price_sats === 0 ? OfferPriceType.Spontaneous : OfferPriceType.Fixed const noffer = nofferEncode({ pubkey, offer: offerStr, priceType, relay, price: offer.price_sats || undefined }) @@ -28,10 +19,14 @@ const mapToOfferConfig = (appUserId: string, offer: UserOffer, { pubkey, relay } label: offer.label, price_sats: offer.price_sats, callback_url: offer.callback_url, - expected_data: (offer.expected_data || {}) as Record, + payer_data: offer.payer_data || [], offer_id: offer.offer_id, noffer: noffer, - default_offer: appUserId === offer.app_user_id + default_offer: appUserId === offer.app_user_id, + createdAtUnix: offer.created_at.getTime(), + updatedAtUnix: offer.updated_at.getTime(), + token: offer.bearer_token, + rejectUnauthorized: offer.rejectUnauthorized } } export class OfferManager { @@ -66,7 +61,7 @@ export class OfferManager { async AddUserOffer(ctx: Types.UserContext, req: Types.OfferConfig): Promise { const newOffer = await this.storage.offerStorage.AddUserOffer(ctx.app_user_id, { - expected_data: req.expected_data, + payer_data: req.payer_data, label: req.label, price_sats: req.price_sats, callback_url: req.callback_url, @@ -82,7 +77,7 @@ export class OfferManager { async UpdateUserOffer(ctx: Types.UserContext, req: Types.OfferConfig) { await this.storage.offerStorage.UpdateUserOffer(ctx.app_user_id, req.offer_id, { - expected_data: req.expected_data, + payer_data: req.payer_data, label: req.label, price_sats: req.price_sats, callback_url: req.callback_url, @@ -139,12 +134,8 @@ export class OfferManager { } ValidateExpectedData(userOffer: UserOffer, payerData: any): { passed: false, validated: undefined } | { passed: true, validated: Record } { - const expected = userOffer.expected_data - if (!expected) { - return { passed: true, validated: {} } - } - const expectedKeys = Object.keys(expected) - if (expectedKeys.length === 0) { + const expectedKeys = userOffer.payer_data + if (!expectedKeys || expectedKeys.length === 0) { return { passed: true, validated: {} } } if (typeof payerData !== 'object' || payerData === null) { @@ -193,11 +184,11 @@ export class OfferManager { return this.HandleDefaultUserOffer(offerReq, appId, remote) } if (userOffer.app_user_id === userOffer.offer_id) { - if (userOffer.price_sats !== 0 || userOffer.expected_data) { + if (userOffer.price_sats !== 0 || userOffer.payer_data) { this.logger("default offer has custom price or expected data, resetting") - await this.storage.offerStorage.UpdateUserOffer(userOffer.app_user_id, userOffer.offer_id, { price_sats: 0, expected_data: null }) + await this.storage.offerStorage.UpdateUserOffer(userOffer.app_user_id, userOffer.offer_id, { price_sats: 0, payer_data: null }) userOffer.price_sats = 0 - userOffer.expected_data = null + userOffer.payer_data = null } } let amt = userOffer.price_sats @@ -216,7 +207,9 @@ export class OfferManager { http_callback_url: userOffer.callback_url, payer_identifier: userOffer.app_user_id, receiver_identifier: userOffer.app_user_id, invoice_req: { amountSats: amt, memo: userOffer.label, zap: offerReq.zap }, payer_data: validated ? { data: validated } : undefined, - offer_string: offer + offer_string: offer, + rejectUnauthorized: userOffer.rejectUnauthorized, + token: userOffer.bearer_token }) return { success: true, invoice: res.invoice } } diff --git a/src/services/storage/entity/UserOffer.ts b/src/services/storage/entity/UserOffer.ts index 691a529b..3b30de99 100644 --- a/src/services/storage/entity/UserOffer.ts +++ b/src/services/storage/entity/UserOffer.ts @@ -30,7 +30,14 @@ export class UserOffer { type: 'simple-json', default: null }) - expected_data: Record | null + payer_data: string[] | null + + @Column({ default: "" }) + bearer_token: string + + @Column({ default: true }) + rejectUnauthorized: boolean + @CreateDateColumn() created_at: Date diff --git a/src/services/storage/entity/UserReceivingInvoice.ts b/src/services/storage/entity/UserReceivingInvoice.ts index cef021bc..87fd8908 100644 --- a/src/services/storage/entity/UserReceivingInvoice.ts +++ b/src/services/storage/entity/UserReceivingInvoice.ts @@ -64,6 +64,13 @@ export class UserReceivingInvoice { }) payer_data?: Record + + @Column({ default: true }) + rejectUnauthorized: boolean + + @Column({ default: "" }) + bearer_token: string + @Column({ default: "" }) offer_id?: string diff --git a/src/services/storage/migrations/1752425992291-invoice_callback_urls.ts b/src/services/storage/migrations/1752425992291-invoice_callback_urls.ts new file mode 100644 index 00000000..3867c7f3 --- /dev/null +++ b/src/services/storage/migrations/1752425992291-invoice_callback_urls.ts @@ -0,0 +1,36 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class InvoiceCallbackUrls1752425992291 implements MigrationInterface { + name = 'InvoiceCallbackUrls1752425992291' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_a131e6b58f084f1340538681b5"`); + await queryRunner.query(`CREATE TABLE "temporary_user_receiving_invoice" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "invoice" varchar NOT NULL, "expires_at_unix" integer NOT NULL, "paid_at_unix" integer NOT NULL DEFAULT (0), "internal" boolean NOT NULL DEFAULT (0), "paidByLnd" boolean NOT NULL DEFAULT (0), "callbackUrl" varchar NOT NULL DEFAULT (''), "paid_amount" integer NOT NULL DEFAULT (0), "service_fee" integer NOT NULL DEFAULT (0), "zap_info" text, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "productProductId" varchar, "payerSerialId" integer, "linkedApplicationSerialId" integer, "liquidityProvider" varchar, "payer_data" text, "offer_id" varchar NOT NULL DEFAULT (''), "rejectUnauthorized" boolean NOT NULL DEFAULT (1), "bearer_token" varchar NOT NULL DEFAULT (''), CONSTRAINT "FK_714a8b7d4f89f8a802ca181b789" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_d4bb1e4c60e8a869f1f43ca2e31" FOREIGN KEY ("payerSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_5263bde2a519db9ea608b702ec8" FOREIGN KEY ("productProductId") REFERENCES "product" ("product_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_2c0dfb3483f3e5e7e3cdd5dc71f" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "temporary_user_receiving_invoice"("serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId", "liquidityProvider", "payer_data", "offer_id") SELECT "serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId", "liquidityProvider", "payer_data", "offer_id" FROM "user_receiving_invoice"`); + await queryRunner.query(`DROP TABLE "user_receiving_invoice"`); + await queryRunner.query(`ALTER TABLE "temporary_user_receiving_invoice" RENAME TO "user_receiving_invoice"`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a131e6b58f084f1340538681b5" ON "user_receiving_invoice" ("invoice") `); + + + await queryRunner.query(`CREATE TABLE "temporary_user_offer" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "app_user_id" varchar NOT NULL, "offer_id" varchar NOT NULL, "label" varchar NOT NULL, "price_sats" integer NOT NULL DEFAULT (0), "callback_url" varchar NOT NULL DEFAULT (''), "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "management_pubkey" varchar NOT NULL DEFAULT (''), "payer_data" text, "bearer_token" varchar NOT NULL DEFAULT (''), "rejectUnauthorized" boolean NOT NULL DEFAULT (1), CONSTRAINT "UQ_478f72095abd8a516d3a309a5c5" UNIQUE ("offer_id"))`); + await queryRunner.query(`INSERT INTO "temporary_user_offer"("serial_id", "app_user_id", "offer_id", "label", "price_sats", "callback_url", "created_at", "updated_at", "management_pubkey") SELECT "serial_id", "app_user_id", "offer_id", "label", "price_sats", "callback_url", "created_at", "updated_at", "management_pubkey" FROM "user_offer"`); + await queryRunner.query(`DROP TABLE "user_offer"`); + await queryRunner.query(`ALTER TABLE "temporary_user_offer" RENAME TO "user_offer"`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP INDEX "IDX_a131e6b58f084f1340538681b5"`); + await queryRunner.query(`ALTER TABLE "user_receiving_invoice" RENAME TO "temporary_user_receiving_invoice"`); + await queryRunner.query(`CREATE TABLE "user_receiving_invoice" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "invoice" varchar NOT NULL, "expires_at_unix" integer NOT NULL, "paid_at_unix" integer NOT NULL DEFAULT (0), "internal" boolean NOT NULL DEFAULT (0), "paidByLnd" boolean NOT NULL DEFAULT (0), "callbackUrl" varchar NOT NULL DEFAULT (''), "paid_amount" integer NOT NULL DEFAULT (0), "service_fee" integer NOT NULL DEFAULT (0), "zap_info" text, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "userSerialId" integer, "productProductId" varchar, "payerSerialId" integer, "linkedApplicationSerialId" integer, "liquidityProvider" varchar, "payer_data" text, "offer_id" varchar NOT NULL DEFAULT (''), CONSTRAINT "FK_714a8b7d4f89f8a802ca181b789" FOREIGN KEY ("linkedApplicationSerialId") REFERENCES "application" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_d4bb1e4c60e8a869f1f43ca2e31" FOREIGN KEY ("payerSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_5263bde2a519db9ea608b702ec8" FOREIGN KEY ("productProductId") REFERENCES "product" ("product_id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "FK_2c0dfb3483f3e5e7e3cdd5dc71f" FOREIGN KEY ("userSerialId") REFERENCES "user" ("serial_id") ON DELETE NO ACTION ON UPDATE NO ACTION)`); + await queryRunner.query(`INSERT INTO "user_receiving_invoice"("serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId", "liquidityProvider", "payer_data", "offer_id") SELECT "serial_id", "invoice", "expires_at_unix", "paid_at_unix", "internal", "paidByLnd", "callbackUrl", "paid_amount", "service_fee", "zap_info", "created_at", "updated_at", "userSerialId", "productProductId", "payerSerialId", "linkedApplicationSerialId", "liquidityProvider", "payer_data", "offer_id" FROM "temporary_user_receiving_invoice"`); + await queryRunner.query(`DROP TABLE "temporary_user_receiving_invoice"`); + await queryRunner.query(`CREATE UNIQUE INDEX "IDX_a131e6b58f084f1340538681b5" ON "user_receiving_invoice" ("invoice") `); + + + await queryRunner.query(`ALTER TABLE "user_offer" RENAME TO "temporary_user_offer"`); + await queryRunner.query(`CREATE TABLE "user_offer" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "app_user_id" varchar NOT NULL, "offer_id" varchar NOT NULL, "label" varchar NOT NULL, "price_sats" integer NOT NULL DEFAULT (0), "callback_url" varchar NOT NULL DEFAULT (''), "expected_data" text, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')), "management_pubkey" varchar NOT NULL DEFAULT (''), CONSTRAINT "UQ_478f72095abd8a516d3a309a5c5" UNIQUE ("offer_id"))`); + await queryRunner.query(`INSERT INTO "user_offer"("serial_id", "app_user_id", "offer_id", "label", "price_sats", "callback_url", "created_at", "updated_at", "management_pubkey") SELECT "serial_id", "app_user_id", "offer_id", "label", "price_sats", "callback_url", "created_at", "updated_at", "management_pubkey" FROM "temporary_user_offer"`); + await queryRunner.query(`DROP TABLE "temporary_user_offer"`); + } + +} diff --git a/src/services/storage/migrations/runner.ts b/src/services/storage/migrations/runner.ts index 36e84a5a..ec68c749 100644 --- a/src/services/storage/migrations/runner.ts +++ b/src/services/storage/migrations/runner.ts @@ -19,9 +19,10 @@ import { RootOpsTime1745428134124 } from './1745428134124-root_ops_time.js' import { ChannelEvents1750777346411 } from './1750777346411-channel_events.js' import { ManagementGrant1751307732346 } from './1751307732346-management_grant.js' import { ManagementGrantBanned1751989251513 } from './1751989251513-management_grant_banned.js' +import { InvoiceCallbackUrls1752425992291 } from './1752425992291-invoice_callback_urls.js' export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, - DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513] + DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, InvoiceCallbackUrls1752425992291] export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411] /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise => { await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations) diff --git a/src/services/storage/paymentStorage.ts b/src/services/storage/paymentStorage.ts index 465983fb..e337f9ea 100644 --- a/src/services/storage/paymentStorage.ts +++ b/src/services/storage/paymentStorage.ts @@ -14,7 +14,7 @@ import { Application } from './entity/Application.js'; import TransactionsQueue from "./db/transactionsQueue.js"; import { LoggedEvent } from './eventsLog.js'; import { StorageInterface } from './db/storageInterface.js'; -export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record } +export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record , rejectUnauthorized?: boolean, token?: string} export const defaultInvoiceExpiry = 60 * 60 export default class { dbs: StorageInterface @@ -102,6 +102,8 @@ export default class { liquidityProvider: providerDestination, offer_id: options.offerId, payer_data: options.payerData, + rejectUnauthorized: options.rejectUnauthorized, + bearer_token: options.token }, txId) }