Merge pull request #828 from shocknet/notifications

store and use notifications token
This commit is contained in:
Justin (shocknet) 2025-07-25 12:13:24 -04:00 committed by GitHub
commit 505846fb8b
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
28 changed files with 548 additions and 27 deletions

View file

@ -1,23 +1,23 @@
manifestVersion: 1 manifestVersion: 1
id: lightning-pub id: lightning-pub
category: finance category: finance
name: Lightning.Pub name: Lightning.Pub
version: "1.0.0" version: "1.0.0"
tagline: lightning, nostr, accounts, lnurl, web tagline: lightning, nostr, accounts, lnurl, web
description: >- description: >-
"Pub" is a Nostr-native account system designed "Pub" is a Nostr-native account system designed
to make running Lightning infrastructure for your friends/family/customers to make running Lightning infrastructure for your friends/family/customers
easier than previously thought possible. easier than previously thought possible.
Being Nostr-native eliminates the complexity of configuring your node like a server by using commodity Nostr relays. Being Nostr-native eliminates the complexity of configuring your node like a server by using commodity Nostr relays.
These relays, unlike LNURL proxies, are trustless by nature of Nostr's own encryption spec (NIP44). These relays, unlike LNURL proxies, are trustless by nature of Nostr's own encryption spec (NIP44).
Support for optional services are integrated into Pub for operators seeking backward compatibility with legacy LNURLs and Lightning Addresses. Support for optional services are integrated into Pub for operators seeking backward compatibility with legacy LNURLs and Lightning Addresses.
By solving the networking and programability hurdles, Pub provides Lightning with a 3rd Layer that enables node-runners and By solving the networking and programability hurdles, Pub provides Lightning with a 3rd Layer that enables node-runners and
Uncle Jims to more easily bring their personal network into Bitcoin's permissionless economy. In doing so, Pub runners Uncle Jims to more easily bring their personal network into Bitcoin's permissionless economy. In doing so, Pub runners
can keep the Lightning Network decentralized, with custodial scaling that is free of fiat rails, large banks, can keep the Lightning Network decentralized, with custodial scaling that is free of fiat rails, large banks,
and other forms of high-time-preference shitcoinery. and other forms of high-time-preference shitcoinery.
developer: shocknet developer: shocknet
website: https://shock.network website: https://shock.network
dependencies: dependencies:

View file

@ -18,6 +18,7 @@ import { InviteToken } from "./build/src/services/storage/entity/InviteToken.js"
import { DebitAccess } from "./build/src/services/storage/entity/DebitAccess.js" import { DebitAccess } from "./build/src/services/storage/entity/DebitAccess.js"
import { UserOffer } from "./build/src/services/storage/entity/UserOffer.js" import { UserOffer } from "./build/src/services/storage/entity/UserOffer.js"
import { ManagementGrant } from "./build/src/services/storage/entity/ManagementGrant.js" import { ManagementGrant } from "./build/src/services/storage/entity/ManagementGrant.js"
import { AppUserDevice } from "./build/src/services/storage/entity/AppUserDevice.js"
import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js' import { Initial1703170309875 } from './build/src/services/storage/migrations/1703170309875-initial.js'
import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js' import { LspOrder1718387847693 } from './build/src/services/storage/migrations/1718387847693-lsp_order.js'
@ -43,7 +44,8 @@ export default new DataSource({
PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, DebitToPub1727105758354, UserCbUrl1727112281043,
UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611], UserOffer1733502626042, ManagementGrant1751307732346, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611],
entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment, entities: [User, UserReceivingInvoice, UserReceivingAddress, AddressReceivingTransaction, UserInvoicePayment, UserTransactionPayment,
UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo, TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant], UserBasicAuth, UserEphemeralKey, Product, UserToUserPayment, Application, ApplicationUser, UserToUserPayment, LspOrder, LndNodeInfo,
TrackedProvider, InviteToken, DebitAccess, UserOffer, ManagementGrant, AppUserDevice],
// synchronize: true, // synchronize: true,
}) })
//npx typeorm migration:generate ./src/services/storage/migrations/management_grant_banned -d ./datasource.js //npx typeorm migration:generate ./src/services/storage/migrations/app_user_device -d ./datasource.js

View file

@ -33,6 +33,8 @@
#PORT=1776 #PORT=1776
#JWT_SECRET= #JWT_SECRET=
#SHOCK_PUSH_URL=
#Lightning Address Bridge #Lightning Address Bridge
#BRIDGE_URL=https://shockwallet.app #BRIDGE_URL=https://shockwallet.app

View file

@ -93,6 +93,11 @@ The nostr server will send back a message response, and inside the body there wi
- input: [EnrollAdminTokenRequest](#EnrollAdminTokenRequest) - input: [EnrollAdminTokenRequest](#EnrollAdminTokenRequest)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- EnrollMessagingToken
- auth type: __User__
- input: [MessagingToken](#MessagingToken)
- This methods has an __empty__ __response__ body
- GetAppsMetrics - GetAppsMetrics
- auth type: __Metrics__ - auth type: __Metrics__
- input: [AppsMetricsRequest](#AppsMetricsRequest) - input: [AppsMetricsRequest](#AppsMetricsRequest)
@ -515,6 +520,13 @@ The nostr server will send back a message response, and inside the body there wi
- input: [EnrollAdminTokenRequest](#EnrollAdminTokenRequest) - input: [EnrollAdminTokenRequest](#EnrollAdminTokenRequest)
- This methods has an __empty__ __response__ body - This methods has an __empty__ __response__ body
- EnrollMessagingToken
- auth type: __User__
- http method: __post__
- http route: __/api/user/messaging/enroll__
- input: [MessagingToken](#MessagingToken)
- This methods has an __empty__ __response__ body
- GetApp - GetApp
- auth type: __App__ - auth type: __App__
- http method: __post__ - http method: __post__
@ -1360,6 +1372,10 @@ The nostr server will send back a message response, and inside the body there wi
### ManageOperation ### ManageOperation
- __npub__: _string_ - __npub__: _string_
### MessagingToken
- __device_id__: _string_
- __firebase_messaging_token__: _string_
### MetricsFile ### MetricsFile
### MigrationUpdate ### MigrationUpdate

View file

@ -74,6 +74,7 @@ type Client struct {
EditDebit func(req DebitAuthorizationRequest) error EditDebit func(req DebitAuthorizationRequest) error
EncryptionExchange func(req EncryptionExchangeRequest) error EncryptionExchange func(req EncryptionExchangeRequest) error
EnrollAdminToken func(req EnrollAdminTokenRequest) error EnrollAdminToken func(req EnrollAdminTokenRequest) error
EnrollMessagingToken func(req MessagingToken) error
GetApp func() (*Application, error) GetApp func() (*Application, error)
GetAppUser func(req GetAppUserRequest) (*AppUser, error) GetAppUser func(req GetAppUserRequest) (*AppUser, error)
GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error) GetAppUserLNURLInfo func(req GetAppUserLNURLInfoRequest) (*LnurlPayInfoResponse, error)
@ -667,6 +668,30 @@ func NewClient(params ClientParams) *Client {
} }
return nil return nil
}, },
EnrollMessagingToken: func(req MessagingToken) error {
auth, err := params.RetrieveUserAuth()
if err != nil {
return err
}
finalRoute := "/api/user/messaging/enroll"
body, err := json.Marshal(req)
if err != nil {
return err
}
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
},
GetApp: func() (*Application, error) { GetApp: func() (*Application, error) {
auth, err := params.RetrieveAppAuth() auth, err := params.RetrieveAppAuth()
if err != nil { if err != nil {

View file

@ -445,6 +445,10 @@ type ManageAuthorizations struct {
type ManageOperation struct { type ManageOperation struct {
Npub string `json:"npub"` Npub string `json:"npub"`
} }
type MessagingToken struct {
Device_id string `json:"device_id"`
Firebase_messaging_token string `json:"firebase_messaging_token"`
}
type MetricsFile struct { type MetricsFile struct {
} }
type MigrationUpdate struct { type MigrationUpdate struct {

View file

@ -427,6 +427,18 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'EnrollMessagingToken':
if (!methods.EnrollMessagingToken) {
throw new Error('method EnrollMessagingToken not found' )
} else {
const error = Types.MessagingTokenValidate(operation.req)
opStats.validate = process.hrtime.bigint()
if (error !== null) throw error
await methods.EnrollMessagingToken({...operation, ctx}); responses.push({ status: 'OK' })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'GetDebitAuthorizations': case 'GetDebitAuthorizations':
if (!methods.GetDebitAuthorizations) { if (!methods.GetDebitAuthorizations) {
throw new Error('method GetDebitAuthorizations not found' ) throw new Error('method GetDebitAuthorizations not found' )
@ -847,6 +859,28 @@ export default (methods: Types.ServerMethods, opts: ServerOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } } 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.EnrollMessagingToken) throw new Error('method: EnrollMessagingToken is not implemented')
app.post('/api/user/messaging/enroll', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'EnrollMessagingToken', 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.EnrollMessagingToken) throw new Error('method: EnrollMessagingToken is not implemented')
const authContext = await opts.UserAuthGuard(req.headers['authorization'])
authCtx = authContext
stats.guard = process.hrtime.bigint()
const request = req.body
const error = Types.MessagingTokenValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authContext }, opts.metricsCallback)
const query = req.query
const params = req.params
await methods.EnrollMessagingToken({rpcName:'EnrollMessagingToken', ctx:authContext , req: request})
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.GetApp) throw new Error('method: GetApp is not implemented') if (!opts.allowNotImplementedMethods && !methods.GetApp) throw new Error('method: GetApp is not implemented')
app.post('/api/app/get', async (req, res) => { app.post('/api/app/get', async (req, res) => {
const info: Types.RequestInfo = { rpcName: 'GetApp', batch: false, nostr: false, batchSize: 0} const info: Types.RequestInfo = { rpcName: 'GetApp', batch: false, nostr: false, batchSize: 0}

View file

@ -276,6 +276,17 @@ export default (params: ClientParams) => ({
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
EnrollMessagingToken: async (request: Types.MessagingToken): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveUserAuth()
if (auth === null) throw new Error('retrieveUserAuth() returned null')
let finalRoute = '/api/user/messaging/enroll'
const { data } = await axios.post(params.baseUrl + finalRoute, request, { 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' }
},
GetApp: async (): Promise<ResultError | ({ status: 'OK' }& Types.Application)> => { GetApp: async (): Promise<ResultError | ({ status: 'OK' }& Types.Application)> => {
const auth = await params.retrieveAppAuth() const auth = await params.retrieveAppAuth()
if (auth === null) throw new Error('retrieveAppAuth() returned null') if (auth === null) throw new Error('retrieveAppAuth() returned null')

View file

@ -233,6 +233,18 @@ export default (params: NostrClientParams, send: (to:string, message: NostrRequ
} }
return { status: 'ERROR', reason: 'invalid response' } return { status: 'ERROR', reason: 'invalid response' }
}, },
EnrollMessagingToken: async (request: Types.MessagingToken): Promise<ResultError | ({ status: 'OK' })> => {
const auth = await params.retrieveNostrUserAuth()
if (auth === null) throw new Error('retrieveNostrUserAuth() returned null')
const nostrRequest: NostrRequest = {}
nostrRequest.body = request
const data = await send(params.pubDestination, {rpcName:'EnrollMessagingToken',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' }
},
GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => { GetAppsMetrics: async (request: Types.AppsMetricsRequest): Promise<ResultError | ({ status: 'OK' }& Types.AppsMetrics)> => {
const auth = await params.retrieveNostrMetricsAuth() const auth = await params.retrieveNostrMetricsAuth()
if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null') if (auth === null) throw new Error('retrieveNostrMetricsAuth() returned null')

View file

@ -303,6 +303,18 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
callsMetrics.push({ ...opInfo, ...opStats, ...ctx }) callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
} }
break break
case 'EnrollMessagingToken':
if (!methods.EnrollMessagingToken) {
throw new Error('method not defined: EnrollMessagingToken')
} else {
const error = Types.MessagingTokenValidate(operation.req)
opStats.validate = process.hrtime.bigint()
if (error !== null) throw error
await methods.EnrollMessagingToken({...operation, ctx}); responses.push({ status: 'OK' })
opStats.handle = process.hrtime.bigint()
callsMetrics.push({ ...opInfo, ...opStats, ...ctx })
}
break
case 'GetDebitAuthorizations': case 'GetDebitAuthorizations':
if (!methods.GetDebitAuthorizations) { if (!methods.GetDebitAuthorizations) {
throw new Error('method not defined: GetDebitAuthorizations') throw new Error('method not defined: GetDebitAuthorizations')
@ -665,6 +677,22 @@ export default (methods: Types.ServerMethods, opts: NostrOptions) => {
opts.metricsCallback([{ ...info, ...stats, ...authContext }]) 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 } }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 break
case 'EnrollMessagingToken':
try {
if (!methods.EnrollMessagingToken) throw new Error('method: EnrollMessagingToken is not implemented')
const authContext = await opts.NostrUserAuthGuard(req.appId, req.authIdentifier)
stats.guard = process.hrtime.bigint()
authCtx = authContext
const request = req.body
const error = Types.MessagingTokenValidate(request)
stats.validate = process.hrtime.bigint()
if (error !== null) return logErrorAndReturnResponse(error, 'invalid request body', res, logger, { ...info, ...stats, ...authCtx }, opts.metricsCallback)
await methods.EnrollMessagingToken({rpcName:'EnrollMessagingToken', ctx:authContext , req: request})
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 'GetAppsMetrics': case 'GetAppsMetrics':
try { try {
if (!methods.GetAppsMetrics) throw new Error('method: GetAppsMetrics is not implemented') if (!methods.GetAppsMetrics) throw new Error('method: GetAppsMetrics is not implemented')

View file

@ -35,8 +35,8 @@ export type UserContext = {
app_user_id: string app_user_id: string
user_id: string user_id: string
} }
export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeDebit_Input | AuthorizeManage_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetManageAuthorizations_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | ResetManage_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input export type UserMethodInputs = AddProduct_Input | AddUserOffer_Input | AuthorizeDebit_Input | AuthorizeManage_Input | BanDebit_Input | DecodeInvoice_Input | DeleteUserOffer_Input | EditDebit_Input | EnrollAdminToken_Input | EnrollMessagingToken_Input | GetDebitAuthorizations_Input | GetHttpCreds_Input | GetLNURLChannelLink_Input | GetLnurlPayLink_Input | GetLnurlWithdrawLink_Input | GetManageAuthorizations_Input | GetPaymentState_Input | GetUserInfo_Input | GetUserOffer_Input | GetUserOfferInvoices_Input | GetUserOffers_Input | GetUserOperations_Input | NewAddress_Input | NewInvoice_Input | NewProductInvoice_Input | PayAddress_Input | PayInvoice_Input | ResetDebit_Input | ResetManage_Input | RespondToDebit_Input | UpdateCallbackUrl_Input | UpdateUserOffer_Input | UserHealth_Input
export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeDebit_Output | AuthorizeManage_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetManageAuthorizations_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | ResetManage_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output export type UserMethodOutputs = AddProduct_Output | AddUserOffer_Output | AuthorizeDebit_Output | AuthorizeManage_Output | BanDebit_Output | DecodeInvoice_Output | DeleteUserOffer_Output | EditDebit_Output | EnrollAdminToken_Output | EnrollMessagingToken_Output | GetDebitAuthorizations_Output | GetHttpCreds_Output | GetLNURLChannelLink_Output | GetLnurlPayLink_Output | GetLnurlWithdrawLink_Output | GetManageAuthorizations_Output | GetPaymentState_Output | GetUserInfo_Output | GetUserOffer_Output | GetUserOfferInvoices_Output | GetUserOffers_Output | GetUserOperations_Output | NewAddress_Output | NewInvoice_Output | NewProductInvoice_Output | PayAddress_Output | PayInvoice_Output | ResetDebit_Output | ResetManage_Output | RespondToDebit_Output | UpdateCallbackUrl_Output | UpdateUserOffer_Output | UserHealth_Output
export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext export type AuthContext = AdminContext | AppContext | GuestContext | GuestWithPubContext | MetricsContext | UserContext
export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest} export type AddApp_Input = {rpcName:'AddApp', req: AddAppRequest}
@ -99,6 +99,9 @@ export type EncryptionExchange_Output = ResultError | { status: 'OK' }
export type EnrollAdminToken_Input = {rpcName:'EnrollAdminToken', req: EnrollAdminTokenRequest} export type EnrollAdminToken_Input = {rpcName:'EnrollAdminToken', req: EnrollAdminTokenRequest}
export type EnrollAdminToken_Output = ResultError | { status: 'OK' } export type EnrollAdminToken_Output = ResultError | { status: 'OK' }
export type EnrollMessagingToken_Input = {rpcName:'EnrollMessagingToken', req: MessagingToken}
export type EnrollMessagingToken_Output = ResultError | { status: 'OK' }
export type GetApp_Input = {rpcName:'GetApp'} export type GetApp_Input = {rpcName:'GetApp'}
export type GetApp_Output = ResultError | ({ status: 'OK' } & Application) export type GetApp_Output = ResultError | ({ status: 'OK' } & Application)
@ -342,6 +345,7 @@ export type ServerMethods = {
EditDebit?: (req: EditDebit_Input & {ctx: UserContext }) => Promise<void> EditDebit?: (req: EditDebit_Input & {ctx: UserContext }) => Promise<void>
EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise<void> EncryptionExchange?: (req: EncryptionExchange_Input & {ctx: GuestContext }) => Promise<void>
EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise<void> EnrollAdminToken?: (req: EnrollAdminToken_Input & {ctx: UserContext }) => Promise<void>
EnrollMessagingToken?: (req: EnrollMessagingToken_Input & {ctx: UserContext }) => Promise<void>
GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise<Application> GetApp?: (req: GetApp_Input & {ctx: AppContext }) => Promise<Application>
GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise<AppUser> GetAppUser?: (req: GetAppUser_Input & {ctx: AppContext }) => Promise<AppUser>
GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise<LnurlPayInfoResponse> GetAppUserLNURLInfo?: (req: GetAppUserLNURLInfo_Input & {ctx: AppContext }) => Promise<LnurlPayInfoResponse>
@ -2616,6 +2620,29 @@ export const ManageOperationValidate = (o?: ManageOperation, opts: ManageOperati
return null return null
} }
export type MessagingToken = {
device_id: string
firebase_messaging_token: string
}
export const MessagingTokenOptionalFields: [] = []
export type MessagingTokenOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
device_id_CustomCheck?: (v: string) => boolean
firebase_messaging_token_CustomCheck?: (v: string) => boolean
}
export const MessagingTokenValidate = (o?: MessagingToken, opts: MessagingTokenOptions = {}, path: string = 'MessagingToken::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.device_id !== 'string') return new Error(`${path}.device_id: is not a string`)
if (opts.device_id_CustomCheck && !opts.device_id_CustomCheck(o.device_id)) return new Error(`${path}.device_id: custom check failed`)
if (typeof o.firebase_messaging_token !== 'string') return new Error(`${path}.firebase_messaging_token: is not a string`)
if (opts.firebase_messaging_token_CustomCheck && !opts.firebase_messaging_token_CustomCheck(o.firebase_messaging_token)) return new Error(`${path}.firebase_messaging_token: custom check failed`)
return null
}
export type MetricsFile = { export type MetricsFile = {
} }
export const MetricsFileOptionalFields: [] = [] export const MetricsFileOptionalFields: [] = []

View file

@ -672,6 +672,12 @@ service LightningPub {
option (http_route) = "/api/user/http_creds"; option (http_route) = "/api/user/http_creds";
option (nostr) = true; option (nostr) = true;
} }
rpc EnrollMessagingToken(structs.MessagingToken) returns (structs.Empty){
option (auth_type) = "User";
option (http_method) = "post";
option (http_route) = "/api/user/messaging/enroll";
option (nostr) = true;
}
rpc BatchUser(structs.Empty) returns (structs.Empty){ rpc BatchUser(structs.Empty) returns (structs.Empty){
option (auth_type) = "User"; option (auth_type) = "User";
option (http_method) = "post"; option (http_method) = "post";

View file

@ -812,4 +812,7 @@ message ProvidersDisruption {
repeated ProviderDisruption disruptions = 1; repeated ProviderDisruption disruptions = 1;
} }
message MessagingToken {
string device_id = 1;
string firebase_messaging_token = 2;
}

View file

@ -0,0 +1,51 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
import axios from 'axios'
import * as Types from './types.js'
export type ResultError = { status: 'ERROR', reason: string }
export type ClientParams = {
baseUrl: string
retrieveAdminAuth: () => Promise<string | null>
retrieveGuestAuth: () => Promise<string | null>
retrieveNostrAppAuth: (rawBody: string, reqUrl: string, httpMethod: string) => Promise<string | null>
encryptCallback: (plain: any) => Promise<any>
decryptCallback: (encrypted: any) => Promise<any>
deviceId: string
checkResult?: true
}
export default (params: ClientParams) => ({
EnrollServicePub: async (request: Types.ServiceNpub): Promise<ResultError | ({ status: 'OK' })> => {
let finalRoute = '/api/admin/service/enroll'
const auth = await params.retrieveAdminAuth()
if (auth === null) throw new Error('retrieveAdminAuth() returned null')
const { data } = await axios.post(params.baseUrl + finalRoute, request, { 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' }
},
Health: async (): Promise<ResultError | ({ status: 'OK' })> => {
let finalRoute = '/api/health'
const auth = await params.retrieveGuestAuth()
if (auth === null) throw new Error('retrieveGuestAuth() returned null')
const { data } = await axios.get(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' }
},
SendNotification: async (request: Types.Notification): Promise<ResultError | ({ status: 'OK' })> => {
let finalRoute = '/api/user/notification'
const rawBody = JSON.stringify(request)
const auth = await params.retrieveNostrAppAuth(rawBody, finalRoute, 'post')
if (auth === null) throw new Error('retrieveNostrAppAuth() returned null')
const { data } = await axios.post(params.baseUrl + finalRoute, rawBody, { 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' }
},
})

View file

@ -0,0 +1,104 @@
// This file was autogenerated from a .proto file, DO NOT EDIT!
export type ResultError = { status: 'ERROR', reason: string }
export type RequestInfo = { rpcName: string, batch: boolean, nostr: boolean, batchSize: number }
export type RequestStats = { startMs: number, start: bigint, parse: bigint, guard: bigint, validate: bigint, handle: bigint }
export type RequestMetric = AuthContext & RequestInfo & RequestStats & { error?: string }
export type ProtoSocketState = 'OPEN' | 'CLOSED'
export type ProtoSocket<T> = {
getState: () => ProtoSocketState
send: (res: T, err: Error | null) => void
}
export type AdminContext = {
admin_id: string
}
export type AdminMethodInputs = EnrollServicePub_Input
export type AdminMethodOutputs = EnrollServicePub_Output
export type GuestContext = {
}
export type GuestMethodInputs = Health_Input
export type GuestMethodOutputs = Health_Output
export type NostrAppContext = {
nostr_app_npub: string
}
export type NostrAppMethodInputs = SendNotification_Input
export type NostrAppMethodOutputs = SendNotification_Output
export type AuthContext = AdminContext | GuestContext | NostrAppContext
export type EnrollServicePub_Input = { rpcName: 'EnrollServicePub', req: ServiceNpub }
export type EnrollServicePub_Output = ResultError | { status: 'OK' }
export type Health_Input = { rpcName: 'Health' }
export type Health_Output = ResultError | { status: 'OK' }
export type SendNotification_Input = { rpcName: 'SendNotification', req: Notification }
export type SendNotification_Output = ResultError | { status: 'OK' }
export type ServerMethods = {
EnrollServicePub?: (req: EnrollServicePub_Input & { ctx: AdminContext }) => Promise<void>
Health?: (req: Health_Input & { ctx: GuestContext }) => Promise<void>
SendNotification?: (req: SendNotification_Input & { ctx: NostrAppContext }) => Promise<void>
}
export type OptionsBaseMessage = {
allOptionalsAreSet?: true
}
export type Empty = {
}
export const EmptyOptionalFields: [] = []
export type EmptyOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
}
export const EmptyValidate = (o?: Empty, opts: EmptyOptions = {}, path: string = 'Empty::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')
return null
}
export type Notification = {
data: string
recipient_registration_tokens: string[]
}
export const NotificationOptionalFields: [] = []
export type NotificationOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
data_CustomCheck?: (v: string) => boolean
recipient_registration_tokens_CustomCheck?: (v: string[]) => boolean
}
export const NotificationValidate = (o?: Notification, opts: NotificationOptions = {}, path: string = 'Notification::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.data !== 'string') return new Error(`${path}.data: is not a string`)
if (opts.data_CustomCheck && !opts.data_CustomCheck(o.data)) return new Error(`${path}.data: custom check failed`)
if (!Array.isArray(o.recipient_registration_tokens)) return new Error(`${path}.recipient_registration_tokens: is not an array`)
for (let index = 0; index < o.recipient_registration_tokens.length; index++) {
if (typeof o.recipient_registration_tokens[index] !== 'string') return new Error(`${path}.recipient_registration_tokens[${index}]: is not a string`)
}
if (opts.recipient_registration_tokens_CustomCheck && !opts.recipient_registration_tokens_CustomCheck(o.recipient_registration_tokens)) return new Error(`${path}.recipient_registration_tokens: custom check failed`)
return null
}
export type ServiceNpub = {
npub: string
}
export const ServiceNpubOptionalFields: [] = []
export type ServiceNpubOptions = OptionsBaseMessage & {
checkOptionalsAreSet?: []
npub_CustomCheck?: (v: string) => boolean
}
export const ServiceNpubValidate = (o?: ServiceNpub, opts: ServiceNpubOptions = {}, path: string = 'ServiceNpub::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.npub !== 'string') return new Error(`${path}.npub: is not a string`)
if (opts.npub_CustomCheck && !opts.npub_CustomCheck(o.npub)) return new Error(`${path}.npub: custom check failed`)
return null
}

View file

@ -0,0 +1,61 @@
import { nip98, UnsignedEvent, finalizeEvent } from 'nostr-tools'
import { bytesToHex } from '@noble/hashes/utils'
import { sha256 } from '@noble/hashes/sha256'
import { base64 } from '@scure/base';
import NewClient, { ClientParams } from './autogenerated/http_client.js'
import { ERROR, getLogger } from '../helpers/logger.js'
const utf8Encoder = new TextEncoder()
export type PushPair = { pubkey: string, privateKey: string }
const nip98Kind = 27235
export class ShockPush {
private client: ReturnType<typeof NewClient>
private logger: ReturnType<typeof getLogger>
private serviceBaseUrl: string
private pair: PushPair
constructor(shockPushUrl: string, pair: PushPair) {
this.logger = getLogger({ component: 'shockPush' })
this.serviceBaseUrl = shockPushUrl
this.pair = pair
this.client = NewClient({
baseUrl: this.serviceBaseUrl,
retrieveAdminAuth: async () => { throw new Error('not implemented') },
retrieveGuestAuth: async () => (''),
retrieveNostrAppAuth: async (rawBody, reqUrl, httpMethod) => this.generateNip98Header(rawBody, reqUrl, httpMethod),
encryptCallback: () => { throw new Error('not implemented') },
decryptCallback: () => { throw new Error('not implemented') },
deviceId: '',
})
}
private generateNip98Header = async (raw: string, url: string, method: string) => {
const tags = [
["u", `${this.serviceBaseUrl}${url}`],
["method", method.toUpperCase()]
];
if (raw !== "") {
tags.push(["payload", bytesToHex(sha256(utf8Encoder.encode(raw)))]);
}
const npub = this.pair.pubkey
const event: UnsignedEvent = {
created_at: Math.round(Date.now() / 1000),
pubkey: npub,
content: "",
kind: nip98Kind,
tags
}
const signed = finalizeEvent(event, Buffer.from(this.pair.privateKey, 'hex'))
const nip98Header = "Nostr " + base64.encode(utf8Encoder.encode(JSON.stringify(signed)));
return nip98Header
}
SendNotification = async (message: string, messagingTokens: string[]) => {
const res = await this.client.SendNotification({ recipient_registration_tokens: messagingTokens, data: message })
if (res.status !== 'OK') {
this.logger(ERROR, `failed to send notification: ${res.status}`)
}
}
}

View file

@ -111,4 +111,10 @@ export default class {
user_identifier: ctx.app_user_id user_identifier: ctx.app_user_id
}) })
} }
async EnrollMessagingToken(ctx: Types.UserContext, req: Types.MessagingToken): Promise<void> {
const app = await this.storage.applicationStorage.GetApplication(ctx.app_id);
const user = await this.storage.applicationStorage.GetApplicationUser(app, ctx.app_user_id);
await this.storage.applicationStorage.UpdateAppUserMessagingToken(user.identifier, req.device_id, req.firebase_messaging_token);
}
} }

View file

@ -316,4 +316,6 @@ export default class {
await this.storage.applicationStorage.SetInviteTokenAsUsed(inviteToken); await this.storage.applicationStorage.SetInviteTokenAsUsed(inviteToken);
} }
} }

View file

@ -1,3 +1,4 @@
import { nip44 } from 'nostr-tools'
import fetch from "node-fetch" import fetch from "node-fetch"
import Storage, { LoadStorageSettingsFromEnv } from '../storage/index.js' import Storage, { LoadStorageSettingsFromEnv } from '../storage/index.js'
import * as Types from '../../../proto/autogenerated/ts/types.js' import * as Types from '../../../proto/autogenerated/ts/types.js'
@ -28,6 +29,8 @@ import { parse } from "uri-template"
import webRTC from "../webRTC/index.js" import webRTC from "../webRTC/index.js"
import { ManagementManager } from "./managementManager.js" import { ManagementManager } from "./managementManager.js"
import { Agent } from "https" import { Agent } from "https"
import { NotificationsManager } from "./notificationsManager.js"
import { ApplicationUser } from '../storage/entity/ApplicationUser.js'
type UserOperationsSub = { type UserOperationsSub = {
id: string id: string
@ -58,6 +61,7 @@ export default class {
utils: Utils utils: Utils
rugPullTracker: RugPullTracker rugPullTracker: RugPullTracker
unlocker: Unlocker unlocker: Unlocker
notificationsManager: NotificationsManager
//webRTC: webRTC //webRTC: webRTC
nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") } nostrSend: NostrSend = () => { getLogger({})("nostr send not initialized yet") }
nostrProcessPing: (() => Promise<void>) | null = null nostrProcessPing: (() => Promise<void>) | null = null
@ -81,6 +85,7 @@ export default class {
this.debitManager = new DebitManager(this.storage, this.lnd, this.applicationManager) this.debitManager = new DebitManager(this.storage, this.lnd, this.applicationManager)
this.offerManager = new OfferManager(this.storage, this.settings, this.lnd, this.applicationManager, this.productManager, this.liquidityManager) this.offerManager = new OfferManager(this.storage, this.settings, this.lnd, this.applicationManager, this.productManager, this.liquidityManager)
this.managementManager = new ManagementManager(this.storage, this.settings) this.managementManager = new ManagementManager(this.storage, this.settings)
this.notificationsManager = new NotificationsManager(this.settings.shockPushBaseUrl)
//this.webRTC = new webRTC(this.storage, this.utils) //this.webRTC = new webRTC(this.storage, this.utils)
} }
@ -287,12 +292,13 @@ export default class {
async triggerPaidCallback(log: PubLogger, url: string, async triggerPaidCallback(log: PubLogger, url: string,
{ invoice, amount, payerData, token, rejectUnauthorized }: { invoice, amount, payerData, token, rejectUnauthorized }:
{ invoice: string, {
amount: number, invoice: string,
payerData?: Record<string, string>, amount: number,
token?: string, payerData?: Record<string, string>,
rejectUnauthorized?: boolean token?: string,
} rejectUnauthorized?: boolean
}
) { ) {
if (!url) { if (!url) {
return return
@ -357,8 +363,27 @@ export default class {
getLogger({ appName: app.name })("cannot notify user, not a nostr user") getLogger({ appName: app.name })("cannot notify user, not a nostr user")
return return
} }
const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' } const message: Types.LiveUserOperation & { requestId: string, status: 'OK' } = { operation: op, requestId: "GetLiveUserOperations", status: 'OK' }
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: JSON.stringify(message), pub: user.nostr_public_key }) const j = JSON.stringify(message)
this.nostrSend({ type: 'app', appId: app.app_id }, { type: 'content', content: j, pub: user.nostr_public_key })
this.SendEncryptedNotification(app, user, op)
}
async SendEncryptedNotification(app: Application, appUser: ApplicationUser, op: Types.UserOperation) {
const devices = await this.storage.applicationStorage.GetAppUserDevices(appUser.identifier)
if (devices.length === 0 || !app.nostr_public_key || !app.nostr_private_key || !appUser.nostr_public_key) {
return
}
const tokens = devices.map(d => d.firebase_messaging_token)
const ck = nip44.getConversationKey(Buffer.from(app.nostr_private_key, 'hex'), appUser.nostr_public_key)
const j = JSON.stringify(op)
const encrypted = nip44.encrypt(j, ck)
const encryptedData: { encrypted: string, app_npub_hex: string } = { encrypted, app_npub_hex: app.nostr_public_key }
this.notificationsManager.SendNotification(JSON.stringify(encryptedData), tokens, {
pubkey: app.nostr_public_key!,
privateKey: app.nostr_private_key!
})
} }
async UpdateBeacon(app: Application, content: { type: 'service', name: string }) { async UpdateBeacon(app: Application, content: { type: 'service', name: string }) {

View file

@ -0,0 +1,31 @@
import { PushPair, ShockPush } from "../ShockPush/index.js"
import { getLogger, PubLogger } from "../helpers/logger.js"
export class NotificationsManager {
private shockPushBaseUrl: string
private clients: Record<string, ShockPush> = {}
private logger: PubLogger
constructor(shockPushBaseUrl: string) {
this.shockPushBaseUrl = shockPushBaseUrl
this.logger = getLogger({ component: 'notificationsManager' })
}
private getClient = (pair: PushPair) => {
const client = this.clients[pair.pubkey]
if (client) {
return client
}
const newClient = new ShockPush(this.shockPushBaseUrl, pair)
this.clients[pair.pubkey] = newClient
return newClient
}
SendNotification = async (message: string, messagingTokens: string[], pair: PushPair) => {
if (!this.shockPushBaseUrl) {
this.logger("ShockPush is not configured, skipping notification")
return
}
const client = this.getClient(pair)
await client.SendNotification(message, messagingTokens)
}
}

View file

@ -39,6 +39,7 @@ export type MainSettings = {
bridgeUrl: string, bridgeUrl: string,
allowResetMetricsStorages: boolean allowResetMetricsStorages: boolean
allowHttpUpgrade: boolean allowHttpUpgrade: boolean
shockPushBaseUrl: string
} }
export type BitcoinCoreSettings = { export type BitcoinCoreSettings = {
@ -81,7 +82,8 @@ export const LoadMainSettingsFromEnv = (): MainSettings => {
lnurlMetaText: process.env.LNURL_META_TEXT || "LNURL via Lightning.pub", lnurlMetaText: process.env.LNURL_META_TEXT || "LNURL via Lightning.pub",
bridgeUrl: process.env.BRIDGE_URL || "https://shockwallet.app", bridgeUrl: process.env.BRIDGE_URL || "https://shockwallet.app",
allowResetMetricsStorages: process.env.ALLOW_RESET_METRICS_STORAGES === 'true' || false, allowResetMetricsStorages: process.env.ALLOW_RESET_METRICS_STORAGES === 'true' || false,
allowHttpUpgrade: process.env.ALLOW_HTTP_UPGRADE === 'true' || false allowHttpUpgrade: process.env.ALLOW_HTTP_UPGRADE === 'true' || false,
shockPushBaseUrl: process.env.SHOCK_PUSH_URL || ""
} }
} }

View file

@ -426,5 +426,13 @@ export default (mainHandler: Main): Types.ServerMethods => {
GetHttpCreds: async ({ ctx }) => { GetHttpCreds: async ({ ctx }) => {
return mainHandler.appUserManager.GetHttpCreds(ctx) return mainHandler.appUserManager.GetHttpCreds(ctx)
}, },
EnrollMessagingToken: async ({ ctx, req }) => {
const err = Types.MessagingTokenValidate(req, {
device_id_CustomCheck: id => id !== '',
firebase_messaging_token_CustomCheck: token => token !== ''
})
if (err != null) throw new Error(err.message)
return mainHandler.appUserManager.EnrollMessagingToken(ctx, req)
},
} }
} }

View file

@ -8,6 +8,7 @@ import { getLogger } from '../helpers/logger.js';
import { User } from './entity/User.js'; import { User } from './entity/User.js';
import { InviteToken } from './entity/InviteToken.js'; import { InviteToken } from './entity/InviteToken.js';
import { StorageInterface } from './db/storageInterface.js'; import { StorageInterface } from './db/storageInterface.js';
import { AppUserDevice } from './entity/AppUserDevice.js';
export default class { export default class {
dbs: StorageInterface dbs: StorageInterface
userStorage: UserStorage userStorage: UserStorage
@ -178,4 +179,23 @@ export default class {
return this.dbs.Update<InviteToken>('InviteToken', inviteToken, { used: true }) return this.dbs.Update<InviteToken>('InviteToken', inviteToken, { used: true })
} }
async UpdateAppUserMessagingToken(appUserId: string, deviceId: string, firebaseMessagingToken: string) {
const existing = await this.dbs.FindOne<AppUserDevice>('AppUserDevice', { where: { app_user_id: appUserId, device_id: deviceId } })
if (!existing) {
return this.dbs.CreateAndSave<AppUserDevice>('AppUserDevice', {
app_user_id: appUserId,
device_id: deviceId,
firebase_messaging_token: firebaseMessagingToken
})
}
if (existing.firebase_messaging_token === firebaseMessagingToken) {
return
}
return this.dbs.Update<AppUserDevice>('AppUserDevice', existing.serial_id, { firebase_messaging_token: firebaseMessagingToken })
}
async GetAppUserDevices(appUserId: string, txId?: string) {
return this.dbs.Find<AppUserDevice>('AppUserDevice', { where: { app_user_id: appUserId } }, txId)
}
} }

View file

@ -26,6 +26,7 @@ import { RootOperation } from "../entity/RootOperation.js"
import { UserOffer } from "../entity/UserOffer.js" import { UserOffer } from "../entity/UserOffer.js"
import { ManagementGrant } from "../entity/ManagementGrant.js" import { ManagementGrant } from "../entity/ManagementGrant.js"
import { ChannelEvent } from "../entity/ChannelEvent.js" import { ChannelEvent } from "../entity/ChannelEvent.js"
import { AppUserDevice } from "../entity/AppUserDevice.js"
export type DbSettings = { export type DbSettings = {
@ -68,7 +69,8 @@ export const MainDbEntities = {
'DebitAccess': DebitAccess, 'DebitAccess': DebitAccess,
'UserOffer': UserOffer, 'UserOffer': UserOffer,
'Product': Product, 'Product': Product,
'ManagementGrant': ManagementGrant 'ManagementGrant': ManagementGrant,
'AppUserDevice': AppUserDevice
} }
export type MainDbNames = keyof typeof MainDbEntities export type MainDbNames = keyof typeof MainDbEntities
export const MainDbEntitiesNames = Object.keys(MainDbEntities) export const MainDbEntitiesNames = Object.keys(MainDbEntities)

View file

@ -0,0 +1,22 @@
import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from "typeorm";
@Entity()
export class AppUserDevice {
@PrimaryGeneratedColumn()
serial_id: number
@Column()
app_user_id: string
@Column()
device_id: string
@Column()
firebase_messaging_token: string
@CreateDateColumn()
created_at: Date
@UpdateDateColumn()
updated_at: Date
}

View file

@ -0,0 +1,13 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AppUserDevice1753285173175 implements MigrationInterface {
name = 'AppUserDevice1753285173175'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "app_user_device" ("serial_id" integer PRIMARY KEY AUTOINCREMENT NOT NULL, "app_user_id" varchar NOT NULL, "device_id" varchar NOT NULL, "firebase_messaging_token" varchar NOT NULL, "created_at" datetime NOT NULL DEFAULT (datetime('now')), "updated_at" datetime NOT NULL DEFAULT (datetime('now')))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "app_user_device"`);
}
}

View file

@ -20,11 +20,15 @@ import { ChannelEvents1750777346411 } from './1750777346411-channel_events.js'
import { ManagementGrant1751307732346 } from './1751307732346-management_grant.js' import { ManagementGrant1751307732346 } from './1751307732346-management_grant.js'
import { ManagementGrantBanned1751989251513 } from './1751989251513-management_grant_banned.js' import { ManagementGrantBanned1751989251513 } from './1751989251513-management_grant_banned.js'
import { InvoiceCallbackUrls1752425992291 } from './1752425992291-invoice_callback_urls.js' import { InvoiceCallbackUrls1752425992291 } from './1752425992291-invoice_callback_urls.js'
import { AppUserDevice1753285173175 } from './1753285173175-app_user_device.js'
import { OldSomethingLeftover1753106599604 } from './1753106599604-old_something_leftover.js' import { OldSomethingLeftover1753106599604 } from './1753106599604-old_something_leftover.js'
import { UserReceivingInvoiceIdx1753109184611 } from './1753109184611-user_receiving_invoice_idx.js' import { UserReceivingInvoiceIdx1753109184611 } from './1753109184611-user_receiving_invoice_idx.js'
export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189, export const allMigrations = [Initial1703170309875, LspOrder1718387847693, LiquidityProvider1719335699480, LndNodeInfo1720187506189,
TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264, TrackedProvider1720814323679, CreateInviteTokenTable1721751414878, PaymentIndex1721760297610, DebitAccess1726496225078, DebitAccessFixes1726685229264,
DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611] DebitToPub1727105758354, UserCbUrl1727112281043, UserOffer1733502626042, ManagementGrant1751307732346, ManagementGrantBanned1751989251513, InvoiceCallbackUrls1752425992291, OldSomethingLeftover1753106599604, UserReceivingInvoiceIdx1753109184611, AppUserDevice1753285173175]
export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411] export const allMetricsMigrations = [LndMetrics1703170330183, ChannelRouting1709316653538, HtlcCount1724266887195, BalanceEvents1724860966825, RootOps1732566440447, RootOpsTime1745428134124, ChannelEvents1750777346411]
/* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => { /* export const TypeOrmMigrationRunner = async (log: PubLogger, storageManager: Storage, settings: DbSettings, arg: string | undefined): Promise<boolean> => {
await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations) await connectAndMigrate(log, storageManager, allMigrations, allMetricsMigrations)

View file

@ -14,7 +14,7 @@ import { Application } from './entity/Application.js';
import TransactionsQueue from "./db/transactionsQueue.js"; import TransactionsQueue from "./db/transactionsQueue.js";
import { LoggedEvent } from './eventsLog.js'; import { LoggedEvent } from './eventsLog.js';
import { StorageInterface } from './db/storageInterface.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<string, string> , rejectUnauthorized?: boolean, token?: string} export type InboundOptionals = { product?: Product, callbackUrl?: string, expiry: number, expectedPayer?: User, linkedApplication?: Application, zapInfo?: ZapInfo, offerId?: string, payerData?: Record<string, string>, rejectUnauthorized?: boolean, token?: string }
export const defaultInvoiceExpiry = 60 * 60 export const defaultInvoiceExpiry = 60 * 60
export default class { export default class {
dbs: StorageInterface dbs: StorageInterface