@bsv/wallet-toolbox 1.2.27 → 1.2.30
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/docs/client.md +7 -22
- package/docs/wallet.md +7 -22
- package/out/src/WalletPermissionsManager.d.ts +4 -4
- package/out/src/WalletPermissionsManager.d.ts.map +1 -1
- package/out/src/WalletPermissionsManager.js +2 -2
- package/out/src/WalletPermissionsManager.js.map +1 -1
- package/out/src/WalletSettingsManager.d.ts.map +1 -1
- package/out/src/WalletSettingsManager.js +11 -12
- package/out/src/WalletSettingsManager.js.map +1 -1
- package/out/src/__tests/WalletPermissionsManager.encryption.test.js +8 -7
- package/out/src/__tests/WalletPermissionsManager.encryption.test.js.map +1 -1
- package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts +2 -0
- package/out/src/__tests/WalletPermissionsManager.fixtures.d.ts.map +1 -1
- package/out/src/__tests/WalletPermissionsManager.fixtures.js +5 -0
- package/out/src/__tests/WalletPermissionsManager.fixtures.js.map +1 -1
- package/out/test/Wallet/local/localWallet2.man.test.js +58 -34
- package/out/test/Wallet/local/localWallet2.man.test.js.map +1 -1
- package/out/tsconfig.all.tsbuildinfo +1 -1
- package/package.json +4 -4
- package/src/WalletPermissionsManager.ts +10 -10
- package/src/WalletSettingsManager.ts +13 -12
- package/src/__tests/WalletPermissionsManager.encryption.test.ts +8 -7
- package/src/__tests/WalletPermissionsManager.fixtures.ts +6 -0
- package/test/Wallet/local/localWallet2.man.test.ts +65 -35
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@bsv/wallet-toolbox",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.30",
|
|
4
4
|
"description": "BRC100 conforming wallet, wallet storage and wallet signer components",
|
|
5
5
|
"main": "./out/src/index.js",
|
|
6
6
|
"types": "./out/src/index.d.ts",
|
|
@@ -30,9 +30,9 @@
|
|
|
30
30
|
},
|
|
31
31
|
"homepage": "https://github.com/bitcoin-sv/wallet-toolbox#readme",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@bsv/auth-express-middleware": "^1.
|
|
34
|
-
"@bsv/payment-express-middleware": "^1.0.
|
|
35
|
-
"@bsv/sdk": "^1.4.
|
|
33
|
+
"@bsv/auth-express-middleware": "^1.1.2",
|
|
34
|
+
"@bsv/payment-express-middleware": "^1.0.4",
|
|
35
|
+
"@bsv/sdk": "^1.4.12",
|
|
36
36
|
"express": "^4.21.2",
|
|
37
37
|
"knex": "^3.1.0",
|
|
38
38
|
"mysql2": "^3.12.0",
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { WalletInterface, Utils, PushDrop, LockingScript, Transaction } from '@bsv/sdk'
|
|
1
|
+
import { WalletInterface, Utils, PushDrop, LockingScript, Transaction, WalletProtocol, Base64String } from '@bsv/sdk'
|
|
2
2
|
import { validateCreateActionArgs } from './sdk'
|
|
3
3
|
|
|
4
4
|
////// TODO: ADD SUPPORT FOR ADMIN COUNTERPARTIES BASED ON WALLET STORAGE
|
|
@@ -23,7 +23,7 @@ export interface PermissionRequest {
|
|
|
23
23
|
type: 'protocol' | 'basket' | 'certificate' | 'spending'
|
|
24
24
|
originator: string // The domain or FQDN of the requesting application
|
|
25
25
|
privileged?: boolean // For "protocol" or "certificate" usage, indicating privileged key usage
|
|
26
|
-
protocolID?:
|
|
26
|
+
protocolID?: WalletProtocol // For type='protocol': BRC-43 style (securityLevel, protocolName)
|
|
27
27
|
counterparty?: string // For type='protocol': e.g. target public key or "self"/"anyone"
|
|
28
28
|
|
|
29
29
|
basket?: string // For type='basket': the basket name being requested
|
|
@@ -509,7 +509,7 @@ export class WalletPermissionsManager implements WalletInterface {
|
|
|
509
509
|
}: {
|
|
510
510
|
originator: string
|
|
511
511
|
privileged: boolean
|
|
512
|
-
protocolID:
|
|
512
|
+
protocolID: WalletProtocol
|
|
513
513
|
counterparty: string
|
|
514
514
|
reason?: string
|
|
515
515
|
seekPermission?: boolean
|
|
@@ -939,7 +939,7 @@ export class WalletPermissionsManager implements WalletInterface {
|
|
|
939
939
|
* @param plaintext The metadata to encrypt if configured to do so
|
|
940
940
|
* @returns The encrypted metadata, or the original value if encryption was disabled.
|
|
941
941
|
*/
|
|
942
|
-
private async maybeEncryptMetadata(plaintext: string): Promise<
|
|
942
|
+
private async maybeEncryptMetadata(plaintext: string): Promise<Base64String> {
|
|
943
943
|
if (!this.config.encryptWalletMetadata) {
|
|
944
944
|
return plaintext
|
|
945
945
|
}
|
|
@@ -951,7 +951,7 @@ export class WalletPermissionsManager implements WalletInterface {
|
|
|
951
951
|
},
|
|
952
952
|
this.adminOriginator
|
|
953
953
|
)
|
|
954
|
-
return Utils.
|
|
954
|
+
return Utils.toBase64(ciphertext)
|
|
955
955
|
}
|
|
956
956
|
|
|
957
957
|
/**
|
|
@@ -959,11 +959,11 @@ export class WalletPermissionsManager implements WalletInterface {
|
|
|
959
959
|
* @param ciphertext The metadata to attempt decryption for.
|
|
960
960
|
* @returns The decrypted metadata. If decryption fails, returns the original value instead.
|
|
961
961
|
*/
|
|
962
|
-
private async maybeDecryptMetadata(ciphertext:
|
|
962
|
+
private async maybeDecryptMetadata(ciphertext: Base64String): Promise<string> {
|
|
963
963
|
try {
|
|
964
964
|
const { plaintext } = await this.underlying.decrypt(
|
|
965
965
|
{
|
|
966
|
-
ciphertext: Utils.toArray(ciphertext, '
|
|
966
|
+
ciphertext: Utils.toArray(ciphertext, 'base64'),
|
|
967
967
|
protocolID: WalletPermissionsManager.METADATA_ENCRYPTION_PROTOCOL,
|
|
968
968
|
keyID: '1'
|
|
969
969
|
},
|
|
@@ -985,7 +985,7 @@ export class WalletPermissionsManager implements WalletInterface {
|
|
|
985
985
|
private async findProtocolToken(
|
|
986
986
|
originator: string,
|
|
987
987
|
privileged: boolean,
|
|
988
|
-
protocolID:
|
|
988
|
+
protocolID: WalletProtocol,
|
|
989
989
|
counterparty: string,
|
|
990
990
|
includeExpired: boolean
|
|
991
991
|
): Promise<PermissionToken | undefined> {
|
|
@@ -1489,7 +1489,7 @@ export class WalletPermissionsManager implements WalletInterface {
|
|
|
1489
1489
|
public async hasProtocolPermission(params: {
|
|
1490
1490
|
originator: string
|
|
1491
1491
|
privileged: boolean
|
|
1492
|
-
protocolID:
|
|
1492
|
+
protocolID: WalletProtocol
|
|
1493
1493
|
counterparty: string
|
|
1494
1494
|
}): Promise<boolean> {
|
|
1495
1495
|
try {
|
|
@@ -2369,7 +2369,7 @@ export class WalletPermissionsManager implements WalletInterface {
|
|
|
2369
2369
|
*
|
|
2370
2370
|
* If it violates these rules and the caller is not admin, we consider it "admin-only."
|
|
2371
2371
|
*/
|
|
2372
|
-
private isAdminProtocol(proto:
|
|
2372
|
+
private isAdminProtocol(proto: WalletProtocol): boolean {
|
|
2373
2373
|
const protocolName = proto[1]
|
|
2374
2374
|
if (protocolName.startsWith('admin') || protocolName.startsWith('p ')) {
|
|
2375
2375
|
return true
|
|
@@ -47,7 +47,7 @@ export const DEFAULT_SETTINGS = {
|
|
|
47
47
|
name: 'Babbage Trust Services',
|
|
48
48
|
description: 'Resolves identity information for Babbage-run APIs and Bitcoin infrastructure.',
|
|
49
49
|
iconUrl: 'https://projectbabbage.com/favicon.ico',
|
|
50
|
-
identityKey: '
|
|
50
|
+
identityKey: '03daf815fe38f83da0ad83b5bedc520aa488aef5cbc93a93c67a7fe60406cbffe8',
|
|
51
51
|
trust: 4
|
|
52
52
|
},
|
|
53
53
|
{
|
|
@@ -62,7 +62,7 @@ export const DEFAULT_SETTINGS = {
|
|
|
62
62
|
description: 'Certifies social media handles, phone numbers and emails',
|
|
63
63
|
iconUrl: 'https://socialcert.net/favicon.ico',
|
|
64
64
|
trust: 3,
|
|
65
|
-
identityKey: '
|
|
65
|
+
identityKey: '02cf6cdf466951d8dfc9e7c9367511d0007ed6fba35ed42d425cc412fd6cfd4a17'
|
|
66
66
|
}
|
|
67
67
|
]
|
|
68
68
|
},
|
|
@@ -107,10 +107,10 @@ export class WalletSettingsManager {
|
|
|
107
107
|
*/
|
|
108
108
|
async get(): Promise<WalletSettings> {
|
|
109
109
|
// List outputs in the 'wallet-settings' basket
|
|
110
|
+
// There should only be one settings token
|
|
110
111
|
const results = await this.wallet.listOutputs({
|
|
111
112
|
basket: SETTINGS_BASKET,
|
|
112
|
-
include: 'locking scripts'
|
|
113
|
-
limit: 1 // There should only be one settings token
|
|
113
|
+
include: 'locking scripts'
|
|
114
114
|
})
|
|
115
115
|
|
|
116
116
|
// Return defaults if no settings token is found
|
|
@@ -118,7 +118,9 @@ export class WalletSettingsManager {
|
|
|
118
118
|
return this.config.defaultSettings
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
const { fields } = PushDrop.decode(
|
|
121
|
+
const { fields } = PushDrop.decode(
|
|
122
|
+
LockingScript.fromHex(results.outputs[results.outputs.length - 1].lockingScript!)
|
|
123
|
+
)
|
|
122
124
|
// Parse and return settings token
|
|
123
125
|
return JSON.parse(Utils.toUTF8(fields[0]))
|
|
124
126
|
}
|
|
@@ -164,15 +166,12 @@ export class WalletSettingsManager {
|
|
|
164
166
|
// 1. List the existing token UTXO(s) for the settings basket.
|
|
165
167
|
const existingUtxos = await this.wallet.listOutputs({
|
|
166
168
|
basket: SETTINGS_BASKET,
|
|
167
|
-
include: 'entire transactions'
|
|
168
|
-
limit: 1
|
|
169
|
+
include: 'entire transactions'
|
|
169
170
|
})
|
|
170
171
|
|
|
171
172
|
// This is the "create a new token" path — no signAction, just a new locking script.
|
|
172
173
|
if (!existingUtxos.outputs.length) {
|
|
173
174
|
if (!newLockingScript) {
|
|
174
|
-
// The intention was to clear the token, but no tokn was found to clear.
|
|
175
|
-
// Thus, we are done.
|
|
176
175
|
return true
|
|
177
176
|
}
|
|
178
177
|
await this.wallet.createAction({
|
|
@@ -186,14 +185,15 @@ export class WalletSettingsManager {
|
|
|
186
185
|
}
|
|
187
186
|
],
|
|
188
187
|
options: {
|
|
189
|
-
randomizeOutputs: false
|
|
188
|
+
randomizeOutputs: false,
|
|
189
|
+
acceptDelayedBroadcast: false
|
|
190
190
|
}
|
|
191
191
|
})
|
|
192
192
|
return true
|
|
193
193
|
}
|
|
194
194
|
|
|
195
195
|
// 2. Prepare the token UTXO for consumption.
|
|
196
|
-
const tokenOutput = existingUtxos.outputs[
|
|
196
|
+
const tokenOutput = existingUtxos.outputs[existingUtxos.outputs.length - 1]
|
|
197
197
|
const inputToConsume: CreateActionInput = {
|
|
198
198
|
outpoint: tokenOutput.outpoint,
|
|
199
199
|
unlockingScriptLength: 73,
|
|
@@ -219,7 +219,8 @@ export class WalletSettingsManager {
|
|
|
219
219
|
inputs: [inputToConsume], // input index 0
|
|
220
220
|
outputs,
|
|
221
221
|
options: {
|
|
222
|
-
randomizeOutputs: false
|
|
222
|
+
randomizeOutputs: false,
|
|
223
|
+
acceptDelayedBroadcast: false
|
|
223
224
|
}
|
|
224
225
|
})
|
|
225
226
|
const tx = Transaction.fromBEEF(signableTransaction!.tx)
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { mockUnderlyingWallet, MockedBSV_SDK } from './WalletPermissionsManager.fixtures'
|
|
2
2
|
import { WalletPermissionsManager } from '../WalletPermissionsManager'
|
|
3
3
|
import { jest } from '@jest/globals'
|
|
4
|
+
import { Utils } from '@bsv/sdk'
|
|
4
5
|
|
|
5
6
|
jest.mock('@bsv/sdk', () => MockedBSV_SDK)
|
|
6
7
|
|
|
@@ -62,7 +63,7 @@ describe('WalletPermissionsManager - Metadata Encryption & Decryption', () => {
|
|
|
62
63
|
plaintext: [72, 105] // 'Hi'
|
|
63
64
|
})
|
|
64
65
|
|
|
65
|
-
const ciphertext = 'random-string-representing-ciphertext'
|
|
66
|
+
const ciphertext = Utils.toBase64(Utils.toArray('random-string-representing-ciphertext'))
|
|
66
67
|
const result = await manager['maybeDecryptMetadata'](ciphertext)
|
|
67
68
|
|
|
68
69
|
// We expect underlying.decrypt() to have been called
|
|
@@ -153,19 +154,19 @@ describe('WalletPermissionsManager - Metadata Encryption & Decryption', () => {
|
|
|
153
154
|
totalActions: 1,
|
|
154
155
|
actions: [
|
|
155
156
|
{
|
|
156
|
-
description: 'fake-encrypted-string-for-description',
|
|
157
|
+
description: Utils.toBase64(Utils.toArray('fake-encrypted-string-for-description')),
|
|
157
158
|
inputs: [
|
|
158
159
|
{
|
|
159
160
|
outpoint: 'txid1.0',
|
|
160
|
-
inputDescription: 'fake-encrypted-string-for-inputDesc'
|
|
161
|
+
inputDescription: Utils.toBase64(Utils.toArray('fake-encrypted-string-for-inputDesc'))
|
|
161
162
|
}
|
|
162
163
|
],
|
|
163
164
|
outputs: [
|
|
164
165
|
{
|
|
165
166
|
lockingScript: 'OP_RETURN 1234',
|
|
166
167
|
satoshis: 500,
|
|
167
|
-
outputDescription: 'fake-encrypted-string-for-outputDesc',
|
|
168
|
-
customInstructions: 'fake-encrypted-string-for-customInstr'
|
|
168
|
+
outputDescription: Utils.toBase64(Utils.toArray('fake-encrypted-string-for-outputDesc')),
|
|
169
|
+
customInstructions: Utils.toBase64(Utils.toArray('fake-encrypted-string-for-customInstr'))
|
|
169
170
|
}
|
|
170
171
|
]
|
|
171
172
|
}
|
|
@@ -268,7 +269,7 @@ describe('WalletPermissionsManager - Metadata Encryption & Decryption', () => {
|
|
|
268
269
|
// To simulate, we make decryption pass through.
|
|
269
270
|
underlying.decrypt.mockImplementation(x => x)
|
|
270
271
|
const listResult = await (manager as any).listActions({}, 'nonadmin.com')
|
|
271
|
-
expect(underlying.decrypt).toHaveBeenCalledTimes(
|
|
272
|
+
expect(underlying.decrypt).toHaveBeenCalledTimes(3)
|
|
272
273
|
|
|
273
274
|
// Confirm the returned data is the same as originally provided (plaintext)
|
|
274
275
|
const [first] = listResult.actions
|
|
@@ -297,7 +298,7 @@ describe('WalletPermissionsManager - Metadata Encryption & Decryption', () => {
|
|
|
297
298
|
satoshis: 999,
|
|
298
299
|
lockingScript: 'OP_RETURN something',
|
|
299
300
|
basket: 'some-basket',
|
|
300
|
-
customInstructions: 'fake-encrypted-instructions-string'
|
|
301
|
+
customInstructions: Utils.toBase64(Utils.toArray('fake-encrypted-instructions-string'))
|
|
301
302
|
}
|
|
302
303
|
]
|
|
303
304
|
})
|
|
@@ -153,6 +153,12 @@ export const MockUtils = {
|
|
|
153
153
|
toUTF8: (arr: number[]) => {
|
|
154
154
|
// Converts an array of numbers to a UTF-8 string.
|
|
155
155
|
return String.fromCharCode(...arr)
|
|
156
|
+
},
|
|
157
|
+
|
|
158
|
+
toBase64: (arr: number[]) => {
|
|
159
|
+
// Converts an array of numbers to a Base64 string.
|
|
160
|
+
const binaryStr = String.fromCharCode(...arr)
|
|
161
|
+
return btoa(binaryStr)
|
|
156
162
|
}
|
|
157
163
|
}
|
|
158
164
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { EntitySyncState, sdk, Services, Setup, StorageKnex } from '../../../src'
|
|
2
|
-
import { _tu } from '../../utils/TestUtilsWalletStorage'
|
|
1
|
+
import { EntitySyncState, sdk, Services, Setup, StorageKnex, TableOutput, TableUser } from '../../../src'
|
|
2
|
+
import { _tu, TuEnv } from '../../utils/TestUtilsWalletStorage'
|
|
3
3
|
import { specOpInvalidChange, ValidListOutputsArgs, WERR_REVIEW_ACTIONS } from '../../../src/sdk'
|
|
4
4
|
import {
|
|
5
5
|
burnOneSatTestOutput,
|
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
import { abort } from 'process'
|
|
13
13
|
|
|
14
14
|
import * as dotenv from 'dotenv'
|
|
15
|
+
import { WalletOutput } from '@bsv/sdk'
|
|
15
16
|
dotenv.config()
|
|
16
17
|
|
|
17
18
|
const chain: sdk.Chain = 'main'
|
|
@@ -97,44 +98,73 @@ describe('localWallet2 tests', () => {
|
|
|
97
98
|
await setup.wallet.destroy()
|
|
98
99
|
})
|
|
99
100
|
|
|
100
|
-
test('5
|
|
101
|
-
const env =
|
|
102
|
-
const
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
101
|
+
test('5 review and release all production invalid change utxos', async () => {
|
|
102
|
+
const { env, storage } = await createMainReviewSetup()
|
|
103
|
+
const users = await storage.findUsers({ partial: {} })
|
|
104
|
+
const withInvalid: Record<number, { user: TableUser; outputs: WalletOutput[]; total: number }> = {}
|
|
105
|
+
// [76, 48, 166, 94, 110, 111, 81]
|
|
106
|
+
const vargs: ValidListOutputsArgs = {
|
|
107
|
+
basket: specOpInvalidChange,
|
|
108
|
+
tags: [],
|
|
109
|
+
tagQueryMode: 'all',
|
|
110
|
+
includeLockingScripts: false,
|
|
111
|
+
includeTransactions: false,
|
|
112
|
+
includeCustomInstructions: false,
|
|
113
|
+
includeTags: false,
|
|
114
|
+
includeLabels: false,
|
|
115
|
+
limit: 0,
|
|
116
|
+
offset: 0,
|
|
117
|
+
seekPermission: false,
|
|
118
|
+
knownTxids: []
|
|
119
|
+
}
|
|
120
|
+
for (const user of users) {
|
|
121
|
+
const { userId } = user
|
|
115
122
|
const auth = { userId, identityKey: '' }
|
|
116
|
-
const vargs: ValidListOutputsArgs = {
|
|
117
|
-
basket: specOpInvalidChange,
|
|
118
|
-
tags: [],
|
|
119
|
-
tagQueryMode: 'all',
|
|
120
|
-
includeLockingScripts: false,
|
|
121
|
-
includeTransactions: false,
|
|
122
|
-
includeCustomInstructions: false,
|
|
123
|
-
includeTags: false,
|
|
124
|
-
includeLabels: false,
|
|
125
|
-
limit: 0,
|
|
126
|
-
offset: 0,
|
|
127
|
-
seekPermission: false,
|
|
128
|
-
knownTxids: []
|
|
129
|
-
}
|
|
130
123
|
let r = await storage.listOutputs(auth, vargs)
|
|
131
124
|
if (r.totalOutputs > 0) {
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
const total: number = r.outputs.reduce((s, o) => (s += o.satoshis), 0)
|
|
126
|
+
console.log(`userId ${userId}: ${r.totalOutputs} unspendable utxos, total ${total}, ${user.identityKey}`)
|
|
127
|
+
withInvalid[userId] = { user, outputs: r.outputs, total }
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
if (Object.keys(withInvalid).length > 0) {
|
|
131
|
+
debugger
|
|
132
|
+
// Release invalids
|
|
133
|
+
for (const { user, outputs } of Object.values(withInvalid)) {
|
|
134
|
+
const { userId } = user
|
|
135
|
+
const auth = { userId, identityKey: '' }
|
|
136
|
+
await storage.listOutputs(auth, { ...vargs, tags: ['release'] })
|
|
137
|
+
}
|
|
138
|
+
// Verify
|
|
139
|
+
for (const { user, outputs } of Object.values(withInvalid)) {
|
|
140
|
+
const { userId } = user
|
|
141
|
+
const auth = { userId, identityKey: '' }
|
|
142
|
+
const r = await storage.listOutputs(auth, vargs)
|
|
143
|
+
expect(r.totalOutputs).toBe(0)
|
|
135
144
|
}
|
|
136
|
-
expect(r.totalOutputs).toBe(0)
|
|
137
145
|
}
|
|
138
146
|
await storage.destroy()
|
|
139
147
|
})
|
|
140
148
|
})
|
|
149
|
+
|
|
150
|
+
async function createMainReviewSetup(): Promise<{
|
|
151
|
+
env: TuEnv
|
|
152
|
+
storage: StorageKnex
|
|
153
|
+
services: Services
|
|
154
|
+
}> {
|
|
155
|
+
const env = _tu.getEnv('main')
|
|
156
|
+
const knex = Setup.createMySQLKnex(process.env.MAIN_CLOUD_MYSQL_CONNECTION!)
|
|
157
|
+
const storage = new StorageKnex({
|
|
158
|
+
chain: env.chain,
|
|
159
|
+
knex: knex,
|
|
160
|
+
commissionSatoshis: 0,
|
|
161
|
+
commissionPubKeyHex: undefined,
|
|
162
|
+
feeModel: { model: 'sat/kb', value: 1 }
|
|
163
|
+
})
|
|
164
|
+
const servicesOptions = Services.createDefaultOptions(env.chain)
|
|
165
|
+
if (env.whatsonchainApiKey) servicesOptions.whatsOnChainApiKey = env.whatsonchainApiKey
|
|
166
|
+
const services = new Services(servicesOptions)
|
|
167
|
+
storage.setServices(services)
|
|
168
|
+
await storage.makeAvailable()
|
|
169
|
+
return { env, storage, services }
|
|
170
|
+
}
|