services wired with orderAck
This commit is contained in:
parent
bdf8c7206b
commit
6514018cf3
8 changed files with 190 additions and 77 deletions
|
|
@ -54,7 +54,7 @@
|
||||||
"request-promise": "^4.2.6",
|
"request-promise": "^4.2.6",
|
||||||
"response-time": "^2.3.2",
|
"response-time": "^2.3.2",
|
||||||
"shelljs": "^0.8.2",
|
"shelljs": "^0.8.2",
|
||||||
"shock-common": "32.0.0",
|
"shock-common": "34.0.0",
|
||||||
"socket.io": "2.1.1",
|
"socket.io": "2.1.1",
|
||||||
"text-encoding": "^0.7.0",
|
"text-encoding": "^0.7.0",
|
||||||
"tingodb": "^0.6.1",
|
"tingodb": "^0.6.1",
|
||||||
|
|
|
||||||
|
|
@ -6,8 +6,6 @@ const logger = require('winston')
|
||||||
const Common = require('shock-common')
|
const Common = require('shock-common')
|
||||||
const { Constants, Schema } = Common
|
const { Constants, Schema } = Common
|
||||||
const Gun = require('gun')
|
const Gun = require('gun')
|
||||||
const crypto = require('crypto')
|
|
||||||
const fetch = require('node-fetch')
|
|
||||||
|
|
||||||
const { ErrorCode } = Constants
|
const { ErrorCode } = Constants
|
||||||
|
|
||||||
|
|
@ -25,6 +23,7 @@ const Key = require('./key')
|
||||||
const Utils = require('./utils')
|
const Utils = require('./utils')
|
||||||
const SchemaManager = require('../../schema')
|
const SchemaManager = require('../../schema')
|
||||||
const LNDHealthMananger = require('../../../utils/lightningServices/errors')
|
const LNDHealthMananger = require('../../../utils/lightningServices/errors')
|
||||||
|
const { enrollContentTokens, selfContentToken } = require('../../seed')
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
|
* @typedef {import('./SimpleGUN').GUNNode} GUNNode
|
||||||
|
|
@ -961,39 +960,14 @@ const sendSpontaneousPayment = async (
|
||||||
!isNaN(parseInt(opts.ackInfo, 10))
|
!isNaN(parseInt(opts.ackInfo, 10))
|
||||||
) {
|
) {
|
||||||
//user requested a seed to themselves
|
//user requested a seed to themselves
|
||||||
const numberOfTokens = Number(opts.ackInfo)
|
const numberOfTokens = Number(opts.ackInfo) || 1
|
||||||
if (isNaN(numberOfTokens)) {
|
const seedInfo = selfContentToken()
|
||||||
throw new Error('ackInfo provided is not a valid number')
|
if (!seedInfo) {
|
||||||
}
|
|
||||||
const seedUrl = process.env.TORRENT_SEED_URL
|
|
||||||
const seedToken = process.env.TORRENT_SEED_TOKEN
|
|
||||||
if (!seedUrl || !seedToken) {
|
|
||||||
throw new Error('torrentSeed service not available')
|
throw new Error('torrentSeed service not available')
|
||||||
}
|
}
|
||||||
|
const { seedUrl } = seedInfo
|
||||||
console.log('SEED URL OK')
|
console.log('SEED URL OK')
|
||||||
const tokens = Array(numberOfTokens)
|
const tokens = await enrollContentTokens(numberOfTokens, seedInfo)
|
||||||
for (let i = 0; i < numberOfTokens; i++) {
|
|
||||||
tokens[i] = crypto.randomBytes(32).toString('hex')
|
|
||||||
}
|
|
||||||
/**@param {string} token */
|
|
||||||
const enrollToken = async token => {
|
|
||||||
const reqData = {
|
|
||||||
seed_token: seedToken,
|
|
||||||
wallet_token: token
|
|
||||||
}
|
|
||||||
//@ts-expect-error
|
|
||||||
const res = await fetch(`${seedUrl}/api/enroll_token`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(reqData)
|
|
||||||
})
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error('torrentSeed service currently not available')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Promise.all(tokens.map(enrollToken))
|
|
||||||
console.log('RES SEED OK')
|
console.log('RES SEED OK')
|
||||||
const ackData = JSON.stringify({ seedUrl, tokens })
|
const ackData = JSON.stringify({ seedUrl, tokens })
|
||||||
return {
|
return {
|
||||||
|
|
@ -1132,7 +1106,11 @@ const sendSpontaneousPayment = async (
|
||||||
})
|
})
|
||||||
const myLndPub = LNDHealthMananger.lndPub
|
const myLndPub = LNDHealthMananger.lndPub
|
||||||
if (
|
if (
|
||||||
(opts.type !== 'contentReveal' && opts.type !== 'torrentSeed') ||
|
(opts.type !== 'contentReveal' &&
|
||||||
|
opts.type !== 'torrentSeed' &&
|
||||||
|
opts.type !== 'service' &&
|
||||||
|
opts.type !== 'streamSeed' &&
|
||||||
|
opts.type !== 'product') ||
|
||||||
!orderResponse.ackNode
|
!orderResponse.ackNode
|
||||||
) {
|
) {
|
||||||
SchemaManager.AddOrder({
|
SchemaManager.AddOrder({
|
||||||
|
|
|
||||||
|
|
@ -7,8 +7,6 @@ const isFinite = require('lodash/isFinite')
|
||||||
const isNumber = require('lodash/isNumber')
|
const isNumber = require('lodash/isNumber')
|
||||||
const isNaN = require('lodash/isNaN')
|
const isNaN = require('lodash/isNaN')
|
||||||
const Common = require('shock-common')
|
const Common = require('shock-common')
|
||||||
const crypto = require('crypto')
|
|
||||||
const fetch = require('node-fetch')
|
|
||||||
const {
|
const {
|
||||||
Constants: { ErrorCode },
|
Constants: { ErrorCode },
|
||||||
Schema
|
Schema
|
||||||
|
|
@ -18,6 +16,7 @@ const LightningServices = require('../../../../utils/lightningServices')
|
||||||
const Key = require('../key')
|
const Key = require('../key')
|
||||||
const Utils = require('../utils')
|
const Utils = require('../utils')
|
||||||
const Gun = require('gun')
|
const Gun = require('gun')
|
||||||
|
const { selfContentToken, enrollContentTokens } = require('../../../seed')
|
||||||
|
|
||||||
const getUser = () => require('../../Mediator').getUser()
|
const getUser = () => require('../../Mediator').getUser()
|
||||||
|
|
||||||
|
|
@ -152,6 +151,54 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
`Amount was correctly decrypted, but got a non finite number, decryptedAmount: ${decryptedAmount}`
|
`Amount was correctly decrypted, but got a non finite number, decryptedAmount: ${decryptedAmount}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
const mySecret = require('../../Mediator').getMySecret()
|
||||||
|
/**
|
||||||
|
* @type {string|null}
|
||||||
|
*/
|
||||||
|
let serviceOrderType = null //if the order refers to a service, we take the info from the service before sending the invoice
|
||||||
|
/**
|
||||||
|
* @type {{ seedUrl: string, seedToken: string }|null}
|
||||||
|
*/
|
||||||
|
let serviceOrderContentSeedInfo = null //in case the service is of type 'torrentSeed' or 'streamSeed' this is {seedUrl,seedToken}, can be omitted, in that case, it will be taken from env
|
||||||
|
if (order.targetType === 'service') {
|
||||||
|
console.log('General Service')
|
||||||
|
const { ackInfo: serviceID } = order
|
||||||
|
console.log('ACK INFO')
|
||||||
|
console.log(serviceID)
|
||||||
|
if (!Common.isPopulatedString(serviceID)) {
|
||||||
|
throw new TypeError(`no serviceID provided to orderAck`)
|
||||||
|
}
|
||||||
|
const selectedService = await new Promise(res => {
|
||||||
|
getUser()
|
||||||
|
.get(Key.OFFERED_SERVICES)
|
||||||
|
.get(serviceID)
|
||||||
|
.load(res)
|
||||||
|
})
|
||||||
|
console.log(selectedService)
|
||||||
|
if (!selectedService) {
|
||||||
|
throw new TypeError(`invalid serviceID provided to orderAck`)
|
||||||
|
}
|
||||||
|
const {
|
||||||
|
serviceType,
|
||||||
|
servicePrice,
|
||||||
|
serviceSeedUrl: encSeedUrl, //=
|
||||||
|
serviceSeedToken: encSeedToken //=
|
||||||
|
} = selectedService
|
||||||
|
if (Number(amount) !== Number(servicePrice)) {
|
||||||
|
throw new TypeError(
|
||||||
|
`service price mismatch ${amount} : ${servicePrice}`
|
||||||
|
)
|
||||||
|
}
|
||||||
|
if (serviceType === 'torrentSeed' || serviceType === 'streamSeed') {
|
||||||
|
if (encSeedUrl && encSeedToken) {
|
||||||
|
const seedUrl = await SEA.decrypt(encSeedUrl, mySecret)
|
||||||
|
const seedToken = await SEA.decrypt(encSeedToken, mySecret)
|
||||||
|
serviceOrderContentSeedInfo = { seedUrl, seedToken }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
serviceOrderType = serviceType
|
||||||
|
}
|
||||||
|
|
||||||
const invoiceReq = {
|
const invoiceReq = {
|
||||||
expiry: 36000,
|
expiry: 36000,
|
||||||
|
|
@ -218,7 +265,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
add_index: addIndex,
|
add_index: addIndex,
|
||||||
payment_addr: paymentAddr
|
payment_addr: paymentAddr
|
||||||
} = paidInvoice
|
} = paidInvoice
|
||||||
const orderType = order.targetType
|
const orderType = serviceOrderType || order.targetType
|
||||||
const { ackInfo } = order //a string representing what has been requested
|
const { ackInfo } = order //a string representing what has been requested
|
||||||
switch (orderType) {
|
switch (orderType) {
|
||||||
case 'tip': {
|
case 'tip': {
|
||||||
|
|
@ -269,7 +316,6 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
* @type {Record<string,string>} <contentID,decryptedRef>
|
* @type {Record<string,string>} <contentID,decryptedRef>
|
||||||
*/
|
*/
|
||||||
const contentsToSend = {}
|
const contentsToSend = {}
|
||||||
const mySecret = require('../../Mediator').getMySecret()
|
|
||||||
console.log('SECRET OK')
|
console.log('SECRET OK')
|
||||||
let privateFound = false
|
let privateFound = false
|
||||||
await Common.Utils.asyncForEach(
|
await Common.Utils.asyncForEach(
|
||||||
|
|
@ -294,7 +340,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
'post provided from ackInfo does not contain private content'
|
'post provided from ackInfo does not contain private content'
|
||||||
break //no private content in this post
|
break //no private content in this post
|
||||||
}
|
}
|
||||||
const ackData = { unlockedContents: contentsToSend }
|
const ackData = { unlockedContents: contentsToSend, ackInfo }
|
||||||
const toSend = JSON.stringify(ackData)
|
const toSend = JSON.stringify(ackData)
|
||||||
const encrypted = await SEA.encrypt(toSend, secret)
|
const encrypted = await SEA.encrypt(toSend, secret)
|
||||||
const ordResponse = {
|
const ordResponse = {
|
||||||
|
|
@ -325,43 +371,75 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
}
|
}
|
||||||
case 'torrentSeed': {
|
case 'torrentSeed': {
|
||||||
console.log('TORRENT')
|
console.log('TORRENT')
|
||||||
const numberOfTokens = Number(ackInfo)
|
const numberOfTokens = Number(ackInfo) || 1
|
||||||
if (isNaN(numberOfTokens)) {
|
const seedInfo = selfContentToken()
|
||||||
breakError = 'ackInfo provided is not a valid number'
|
if (!seedInfo && !serviceOrderContentSeedInfo) {
|
||||||
break
|
|
||||||
}
|
|
||||||
const seedUrl = process.env.TORRENT_SEED_URL
|
|
||||||
const seedToken = process.env.TORRENT_SEED_TOKEN
|
|
||||||
if (!seedUrl || !seedToken) {
|
|
||||||
breakError = 'torrentSeed service not available'
|
breakError = 'torrentSeed service not available'
|
||||||
break //service not available
|
break //service not available
|
||||||
}
|
}
|
||||||
console.log('SEED URL OK')
|
const seedInfoReady = serviceOrderContentSeedInfo || seedInfo
|
||||||
const tokens = Array(numberOfTokens)
|
if (!seedInfoReady) {
|
||||||
for (let i = 0; i < numberOfTokens; i++) {
|
breakError = 'torrentSeed service not available'
|
||||||
tokens[i] = crypto.randomBytes(32).toString('hex')
|
break //service not available
|
||||||
}
|
}
|
||||||
/**@param {string} token */
|
const { seedUrl } = seedInfoReady
|
||||||
const enrollToken = async token => {
|
const tokens = await enrollContentTokens(
|
||||||
const reqData = {
|
numberOfTokens,
|
||||||
seed_token: seedToken,
|
seedInfoReady
|
||||||
wallet_token: token
|
)
|
||||||
}
|
|
||||||
//@ts-expect-error
|
|
||||||
const res = await fetch(`${seedUrl}/api/enroll_token`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json'
|
|
||||||
},
|
|
||||||
body: JSON.stringify(reqData)
|
|
||||||
})
|
|
||||||
if (res.status !== 200) {
|
|
||||||
throw new Error('torrentSeed service currently not available')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
await Promise.all(tokens.map(enrollToken))
|
|
||||||
console.log('RES SEED OK')
|
console.log('RES SEED OK')
|
||||||
const ackData = { seedUrl, tokens }
|
const ackData = { seedUrl, tokens, ackInfo }
|
||||||
|
const toSend = JSON.stringify(ackData)
|
||||||
|
const encrypted = await SEA.encrypt(toSend, secret)
|
||||||
|
const serviceResponse = {
|
||||||
|
type: 'orderAck',
|
||||||
|
response: encrypted
|
||||||
|
}
|
||||||
|
console.log('RES SEED SENT')
|
||||||
|
await new Promise((res, rej) => {
|
||||||
|
getUser()
|
||||||
|
.get(Key.ORDER_TO_RESPONSE)
|
||||||
|
.get(ackNode)
|
||||||
|
.put(serviceResponse, ack => {
|
||||||
|
if (ack.err && typeof ack.err !== 'number') {
|
||||||
|
rej(
|
||||||
|
new Error(
|
||||||
|
`Error saving encrypted orderAck to order to response usergraph: ${ack}`
|
||||||
|
)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
res(null)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
console.log('RES SENT SEED')
|
||||||
|
orderMetadata = JSON.stringify(serviceResponse)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
case 'streamSeed': {
|
||||||
|
console.log('STREAM')
|
||||||
|
const numberOfTokens = 1
|
||||||
|
const seedInfo = selfContentToken() //TODO this must change for streams
|
||||||
|
if (!seedInfo && !serviceOrderContentSeedInfo) {
|
||||||
|
breakError = 'torrentSeed service not available'
|
||||||
|
break //service not available
|
||||||
|
}
|
||||||
|
const seedInfoReady = serviceOrderContentSeedInfo || seedInfo
|
||||||
|
if (!seedInfoReady) {
|
||||||
|
breakError = 'torrentSeed service not available'
|
||||||
|
break //service not available
|
||||||
|
}
|
||||||
|
const { seedUrl } = seedInfoReady
|
||||||
|
const tokens = await enrollContentTokens(
|
||||||
|
numberOfTokens,
|
||||||
|
seedInfoReady
|
||||||
|
)
|
||||||
|
console.log('RES SEED OK')
|
||||||
|
const ackData = {
|
||||||
|
seedUrl,
|
||||||
|
tokens,
|
||||||
|
ackInfo
|
||||||
|
}
|
||||||
const toSend = JSON.stringify(ackData)
|
const toSend = JSON.stringify(ackData)
|
||||||
const encrypted = await SEA.encrypt(toSend, secret)
|
const encrypted = await SEA.encrypt(toSend, secret)
|
||||||
const serviceResponse = {
|
const serviceResponse = {
|
||||||
|
|
@ -392,6 +470,7 @@ const listenerForAddr = (addr, SEA) => async (order, orderID) => {
|
||||||
case 'other': //not implemented yet but save them as a coordinate anyways
|
case 'other': //not implemented yet but save them as a coordinate anyways
|
||||||
break
|
break
|
||||||
default:
|
default:
|
||||||
|
breakError = 'invalid service type provided'
|
||||||
return //exit because not implemented
|
return //exit because not implemented
|
||||||
}
|
}
|
||||||
const metadata = breakError ? JSON.stringify(breakError) : orderMetadata
|
const metadata = breakError ? JSON.stringify(breakError) : orderMetadata
|
||||||
|
|
|
||||||
|
|
@ -71,3 +71,5 @@ exports.COORDINATE_INDEX = 'coordinateIndex'
|
||||||
exports.TMP_CHAIN_COORDINATE = 'tmpChainCoordinate'
|
exports.TMP_CHAIN_COORDINATE = 'tmpChainCoordinate'
|
||||||
|
|
||||||
exports.DATE_COORDINATE_INDEX = 'dateCoordinateIndex'
|
exports.DATE_COORDINATE_INDEX = 'dateCoordinateIndex'
|
||||||
|
|
||||||
|
exports.OFFERED_SERVICES = 'offeredServices'
|
||||||
|
|
|
||||||
|
|
@ -7,7 +7,7 @@ const Key = require('../gunDB/contact-api/key')
|
||||||
const lndV2 = require('../../utils/lightningServices/v2')
|
const lndV2 = require('../../utils/lightningServices/v2')
|
||||||
/**
|
/**
|
||||||
* @typedef {import('../gunDB/contact-api/SimpleGUN').ISEA} ISEA
|
* @typedef {import('../gunDB/contact-api/SimpleGUN').ISEA} ISEA
|
||||||
* @typedef { 'spontaneousPayment' | 'tip' | 'torrentSeed' | 'contentReveal' | 'other'|'invoice'|'payment'|'chainTx' } OrderType
|
* @typedef { 'spontaneousPayment' | 'tip' | 'torrentSeed' | 'contentReveal' | 'other'|'invoice'|'payment'|'chainTx' | 'streamSeed' |'service'|'product' } OrderType
|
||||||
*
|
*
|
||||||
* This represents a settled order only, unsettled orders have no coordinate
|
* This represents a settled order only, unsettled orders have no coordinate
|
||||||
* @typedef {object} CoordinateOrder //everything is optional for different types
|
* @typedef {object} CoordinateOrder //everything is optional for different types
|
||||||
|
|
|
||||||
51
services/seed.js
Normal file
51
services/seed.js
Normal file
|
|
@ -0,0 +1,51 @@
|
||||||
|
const crypto = require('crypto')
|
||||||
|
const fetch = require('node-fetch')
|
||||||
|
|
||||||
|
const selfContentToken = () => {
|
||||||
|
const seedUrl = process.env.TORRENT_SEED_URL
|
||||||
|
const seedToken = process.env.TORRENT_SEED_TOKEN
|
||||||
|
if (!seedUrl || !seedToken) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return {seedUrl,seedToken}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {number} nOfTokens
|
||||||
|
* @param {{seedUrl:string,seedToken:string}} param1
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
const enrollContentTokens = async (nOfTokens,{seedUrl,seedToken}) => {
|
||||||
|
const tokens = Array(nOfTokens)
|
||||||
|
for (let i = 0; i < nOfTokens; i++) {
|
||||||
|
tokens[i] = crypto.randomBytes(32).toString('hex')
|
||||||
|
}
|
||||||
|
/**@param {string} token */
|
||||||
|
const enrollToken = async token => {
|
||||||
|
const reqData = {
|
||||||
|
seed_token: seedToken,
|
||||||
|
wallet_token: token
|
||||||
|
}
|
||||||
|
//@ts-expect-error
|
||||||
|
const res = await fetch(`${seedUrl}/api/enroll_token`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
},
|
||||||
|
body: JSON.stringify(reqData)
|
||||||
|
})
|
||||||
|
if (res.status !== 200) {
|
||||||
|
throw new Error('torrentSeed service currently not available')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
await Promise.all(tokens.map(enrollToken))
|
||||||
|
return tokens
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
selfContentToken,
|
||||||
|
enrollContentTokens
|
||||||
|
}
|
||||||
|
|
@ -1197,12 +1197,15 @@ module.exports = async (
|
||||||
type !== 'spontaneousPayment' &&
|
type !== 'spontaneousPayment' &&
|
||||||
type !== 'tip' &&
|
type !== 'tip' &&
|
||||||
type !== 'torrentSeed' &&
|
type !== 'torrentSeed' &&
|
||||||
|
type !== 'streamSeed' &&
|
||||||
type !== 'contentReveal' &&
|
type !== 'contentReveal' &&
|
||||||
|
type !== 'service' &&
|
||||||
|
type !== 'product' &&
|
||||||
type !== 'other'
|
type !== 'other'
|
||||||
) {
|
) {
|
||||||
return res.status(415).json({
|
return res.status(415).json({
|
||||||
field: 'type',
|
field: 'type',
|
||||||
errorMessage: `Only 'spontaneousPayment'| 'tip' | 'torrentSeed' | 'contentReveal' | 'other' payments supported via this endpoint for now.`
|
errorMessage: `Only 'spontaneousPayment'| 'tip' | 'torrentSeed' | 'contentReveal' | 'service' | 'streamSeed' | 'product' |'other' payments supported via this endpoint for now.`
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -6212,10 +6212,10 @@ shellwords@^0.1.1:
|
||||||
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
resolved "https://registry.yarnpkg.com/shellwords/-/shellwords-0.1.1.tgz#d6b9181c1a48d397324c84871efbcfc73fc0654b"
|
||||||
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
integrity sha512-vFwSUfQvqybiICwZY5+DAWIPLKsWO31Q91JSKl3UYv+K5c2QRPzn0qzec6QPu1Qc9eHYItiP3NdJqNVqetYAww==
|
||||||
|
|
||||||
shock-common@32.0.0:
|
shock-common@34.0.0:
|
||||||
version "32.0.0"
|
version "34.0.0"
|
||||||
resolved "https://registry.yarnpkg.com/shock-common/-/shock-common-32.0.0.tgz#bb7f70d7a572783c46aeae7420179935eb5096d4"
|
resolved "https://registry.yarnpkg.com/shock-common/-/shock-common-34.0.0.tgz#30ffbcb136af9bc04b936999a7eebee4e18c67f0"
|
||||||
integrity sha512-1GorUFRpkRGXdKT9PImwnj2orpoJaESU/iD+rvL8sqFMLKazkW9LfLAcRwEWCvytWdKFJ35UO2gN49N8dPiRmA==
|
integrity sha512-i+io2YBh/GLXBz4YURdxg0t//gm2H3dmpkdU8gnEVe7i/ZdabYGhyBgEnUOMy1ZTMunvv/20U8wan9W4VrOaVQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
immer "^6.0.6"
|
immer "^6.0.6"
|
||||||
lodash "^4.17.19"
|
lodash "^4.17.19"
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue