@super-protocol/sp-cli 0.0.2-alpha.1 → 0.0.2-beta.1

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.
@@ -0,0 +1,239 @@
1
+ import { confirm, input, password, select, } from '@inquirer/prompts';
2
+ import { Flags } from '@oclif/core';
3
+ import { StorageType } from '@super-protocol/provider-client';
4
+ import fs from 'node:fs/promises';
5
+ import path from 'node:path';
6
+ import { StoragesEmptyError } from '../../services/storage.service.js';
7
+ import { BaseStorageCommand } from './base.js';
8
+ export default class StorageUpdate extends BaseStorageCommand {
9
+ static description = 'Update the configuration of an existing storage.';
10
+ static examples = [
11
+ '<%= config.bin %> storage update --id=2de3e3a4-0000-1111-2222-333344445555 --fromFile ./storage-update.json',
12
+ '<%= config.bin %> storage update --id=2de3e3a4-0000-1111-2222-333344445555',
13
+ ];
14
+ static flags = {
15
+ fromFile: Flags.string({
16
+ char: 'f',
17
+ description: 'Path to a JSON file that contains UpdateStorageDto payload.',
18
+ }),
19
+ id: Flags.string({
20
+ char: 'i',
21
+ description: 'Storage ID to update',
22
+ }),
23
+ };
24
+ async run() {
25
+ const { flags } = await this.parse(StorageUpdate);
26
+ try {
27
+ const shouldRequestStorages = !flags.fromFile || !flags.id;
28
+ const storages = shouldRequestStorages ? await this.storageService.requestStorage() : undefined;
29
+ let storageId = flags.id;
30
+ if (!storageId) {
31
+ const choices = (storages || []).filter(cp => !cp.isCentralized).map(storage => ({
32
+ name: `${storage.id} (${storage.storageType}) ${storage.bucket}/${storage.prefix} `,
33
+ value: storage.id,
34
+ }));
35
+ if (!choices || choices.length === 0) {
36
+ throw new StoragesEmptyError('No storages available to update');
37
+ }
38
+ storageId = await select({
39
+ choices,
40
+ message: 'Select storage to update',
41
+ });
42
+ }
43
+ if (!storageId) {
44
+ this.error('Storage ID is required');
45
+ }
46
+ const storageToUpdate = storages?.find(storage => storage.id === storageId);
47
+ const updatePayload = flags.fromFile
48
+ ? await this.loadStorageFromFile(flags.fromFile)
49
+ : await this.promptStorageUpdate(storageToUpdate);
50
+ await this.storageService.updateStorage(storageId, updatePayload);
51
+ this.log(`Storage updated: ${storageId}`);
52
+ }
53
+ catch (error) {
54
+ if (error instanceof StoragesEmptyError) {
55
+ this.error('No storages available. Run "sp storage create" first.');
56
+ }
57
+ this.error(error instanceof Error ? error.message : String(error));
58
+ }
59
+ }
60
+ ensureNonEmptyString(value, fieldName) {
61
+ if (typeof value !== 'string' || !value.trim()) {
62
+ this.error(`${fieldName} must be a non-empty string`);
63
+ }
64
+ return value.trim();
65
+ }
66
+ ensureUpdatePayload(payload) {
67
+ if (!payload || typeof payload !== 'object') {
68
+ this.error('Storage update definition must be an object');
69
+ }
70
+ const updateFields = ['bucket', 'prefix', 'storageType', 's3Credentials', 'storjCredentials'];
71
+ const hasUpdates = updateFields.some(key => payload[key] !== undefined);
72
+ if (!hasUpdates) {
73
+ this.error('Update payload must include at least one property');
74
+ }
75
+ if (payload.storageType && !Object.values(StorageType).includes(payload.storageType)) {
76
+ this.error(`Unsupported storage type: ${payload.storageType}`);
77
+ }
78
+ if (payload.s3Credentials) {
79
+ const { readAccessKeyId, readSecretAccessKey, region, writeAccessKeyId, writeSecretAccessKey, } = payload.s3Credentials;
80
+ if (!readAccessKeyId || !readSecretAccessKey || !region || !writeAccessKeyId || !writeSecretAccessKey) {
81
+ this.error('S3 credentials must include readAccessKeyId, readSecretAccessKey, region, writeAccessKeyId and writeSecretAccessKey');
82
+ }
83
+ }
84
+ if (payload.storjCredentials) {
85
+ const { readAccessToken, writeAccessToken } = payload.storjCredentials;
86
+ if (!readAccessToken || !writeAccessToken) {
87
+ this.error('StorJ credentials must include readAccessToken and writeAccessToken');
88
+ }
89
+ }
90
+ const sanitized = {};
91
+ if (payload.bucket !== undefined) {
92
+ sanitized.bucket = this.ensureNonEmptyString(payload.bucket, 'Bucket');
93
+ }
94
+ if (payload.prefix !== undefined) {
95
+ sanitized.prefix = this.ensureNonEmptyString(payload.prefix, 'Prefix');
96
+ }
97
+ if (payload.storageType) {
98
+ sanitized.storageType = payload.storageType;
99
+ }
100
+ if (payload.s3Credentials) {
101
+ sanitized.s3Credentials = {
102
+ readAccessKeyId: this.ensureNonEmptyString(payload.s3Credentials.readAccessKeyId, 'S3 credential "readAccessKeyId"'),
103
+ readSecretAccessKey: this.ensureNonEmptyString(payload.s3Credentials.readSecretAccessKey, 'S3 credential "readSecretAccessKey"'),
104
+ region: this.ensureNonEmptyString(payload.s3Credentials.region, 'S3 credential "region"'),
105
+ writeAccessKeyId: this.ensureNonEmptyString(payload.s3Credentials.writeAccessKeyId, 'S3 credential "writeAccessKeyId"'),
106
+ writeSecretAccessKey: this.ensureNonEmptyString(payload.s3Credentials.writeSecretAccessKey, 'S3 credential "writeSecretAccessKey"'),
107
+ };
108
+ }
109
+ if (payload.storjCredentials) {
110
+ sanitized.storjCredentials = {
111
+ readAccessToken: this.ensureNonEmptyString(payload.storjCredentials.readAccessToken, 'StorJ credential "readAccessToken"'),
112
+ writeAccessToken: this.ensureNonEmptyString(payload.storjCredentials.writeAccessToken, 'StorJ credential "writeAccessToken"'),
113
+ };
114
+ }
115
+ return sanitized;
116
+ }
117
+ async loadStorageFromFile(filePath) {
118
+ const absolutePath = path.isAbsolute(filePath) ? filePath : path.join(this.currentDir, filePath);
119
+ try {
120
+ const fileContent = await fs.readFile(absolutePath, 'utf8');
121
+ const parsed = JSON.parse(fileContent);
122
+ return this.ensureUpdatePayload(parsed);
123
+ }
124
+ catch (error) {
125
+ this.error(`Failed to load storage update definition: ${error instanceof Error ? error.message : String(error)}`);
126
+ }
127
+ }
128
+ async promptS3Credentials() {
129
+ const region = await input({
130
+ message: 'AWS region',
131
+ validate(value) {
132
+ return value ? true : 'Region is required';
133
+ },
134
+ }).then(value => value.trim());
135
+ const readAccessKeyId = await input({
136
+ message: 'Read access key ID',
137
+ validate(value) {
138
+ return value ? true : 'Read access key ID is required';
139
+ },
140
+ }).then(value => value.trim());
141
+ const readSecretAccessKey = await password({
142
+ message: 'Read secret access key',
143
+ validate(value) {
144
+ return value ? true : 'Read secret access key is required';
145
+ },
146
+ }).then(value => value.trim());
147
+ const writeAccessKeyId = await input({
148
+ message: 'Write access key ID',
149
+ validate(value) {
150
+ return value ? true : 'Write access key ID is required';
151
+ },
152
+ }).then(value => value.trim());
153
+ const writeSecretAccessKey = await password({
154
+ message: 'Write secret access key',
155
+ validate(value) {
156
+ return value ? true : 'Write secret access key is required';
157
+ },
158
+ }).then(value => value.trim());
159
+ return {
160
+ readAccessKeyId,
161
+ readSecretAccessKey,
162
+ region,
163
+ writeAccessKeyId,
164
+ writeSecretAccessKey,
165
+ };
166
+ }
167
+ async promptStorageUpdate(existingStorage) {
168
+ const payload = {};
169
+ const bucket = await input({
170
+ default: existingStorage?.bucket ?? '',
171
+ message: 'Bucket name (leave empty to keep current)',
172
+ }).then(value => value.trim());
173
+ if (bucket) {
174
+ payload.bucket = bucket;
175
+ }
176
+ const prefix = await input({
177
+ default: existingStorage?.prefix ?? '',
178
+ message: 'Prefix (leave empty to keep current)',
179
+ }).then(value => value.trim());
180
+ if (prefix) {
181
+ payload.prefix = prefix;
182
+ }
183
+ let newStorageType;
184
+ const shouldChangeType = await confirm({
185
+ default: false,
186
+ message: existingStorage
187
+ ? `Change storage type? (current: ${existingStorage.storageType})`
188
+ : 'Change storage type?',
189
+ });
190
+ if (shouldChangeType) {
191
+ newStorageType = await select({
192
+ choices: [
193
+ { name: 'Amazon S3', value: StorageType.S3 },
194
+ { name: 'StorJ', value: StorageType.StorJ },
195
+ ],
196
+ message: 'Select new storage type',
197
+ });
198
+ payload.storageType = newStorageType;
199
+ }
200
+ const effectiveType = newStorageType ?? existingStorage?.storageType;
201
+ const credentialsRequired = Boolean(newStorageType);
202
+ if (effectiveType) {
203
+ const updateCredentials = credentialsRequired || await confirm({
204
+ default: false,
205
+ message: `Update ${effectiveType} credentials?`,
206
+ });
207
+ if (updateCredentials) {
208
+ if (effectiveType === StorageType.S3) {
209
+ payload.s3Credentials = await this.promptS3Credentials();
210
+ }
211
+ else {
212
+ payload.storjCredentials = await this.promptStorJCredentials();
213
+ }
214
+ }
215
+ }
216
+ if (Object.keys(payload).length === 0) {
217
+ this.error('No changes provided');
218
+ }
219
+ return this.ensureUpdatePayload(payload);
220
+ }
221
+ async promptStorJCredentials() {
222
+ const readAccessToken = await password({
223
+ message: 'Read access token',
224
+ validate(value) {
225
+ return value ? true : 'Read access token is required';
226
+ },
227
+ }).then(value => value.trim());
228
+ const writeAccessToken = await password({
229
+ message: 'Write access token',
230
+ validate(value) {
231
+ return value ? true : 'Write access token is required';
232
+ },
233
+ }).then(value => value.trim());
234
+ return {
235
+ readAccessToken,
236
+ writeAccessToken,
237
+ };
238
+ }
239
+ }
@@ -17,6 +17,7 @@ export declare const cliConfigSchema: import("@sinclair/typebox").TObject<{
17
17
  cookies: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TUnknown>;
18
18
  name: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
19
19
  providerUrl: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
20
+ selectedStorage: import("@sinclair/typebox").TOptional<import("@sinclair/typebox").TString>;
20
21
  }>;
21
22
  export type CliConfig = Static<typeof cliConfigSchema>;
22
23
  export type Account = Static<typeof accountSchema>;
@@ -21,4 +21,5 @@ export const cliConfigSchema = Type.Object({
21
21
  cookies: Type.Optional(Type.Unknown()),
22
22
  name: Type.Optional(Type.String()),
23
23
  providerUrl: Type.Optional(Type.String({ format: 'url' })),
24
+ selectedStorage: Type.Optional(Type.String({ maxLength: 36, minLength: 36 })),
24
25
  });
@@ -1,3 +1,4 @@
1
1
  export declare const FILE_ACCESS_PERMS = 384;
2
2
  export declare const DIR_ACCESS_PERMS = 448;
3
3
  export declare const PROVIDER_URL = "https://api.dp.superprotocol.com";
4
+ export declare const REFRESH_TOKEN_URI = "/api/auth/refresh-access";
package/dist/constants.js CHANGED
@@ -3,3 +3,4 @@ export const FILE_ACCESS_PERMS = 0o600;
3
3
  // Directory permissions. 'drwx------'
4
4
  export const DIR_ACCESS_PERMS = 0o700;
5
5
  export const PROVIDER_URL = 'https://api.dp.superprotocol.com';
6
+ export const REFRESH_TOKEN_URI = '/api/auth/refresh-access';
package/dist/errors.d.ts CHANGED
@@ -6,3 +6,5 @@ export declare class InvalidPrivateKeyError extends Error {
6
6
  }
7
7
  export declare class AuthorizationRequiredError extends Error {
8
8
  }
9
+ export declare class ProviderClientError extends Error {
10
+ }
package/dist/errors.js CHANGED
@@ -6,3 +6,5 @@ export class InvalidPrivateKeyError extends Error {
6
6
  }
7
7
  export class AuthorizationRequiredError extends Error {
8
8
  }
9
+ export class ProviderClientError extends Error {
10
+ }
@@ -29,9 +29,10 @@ export declare class AppContainer {
29
29
  initAccountManager(): this;
30
30
  initConfigFileManager(): this;
31
31
  initConfigManager(): this;
32
- initProviderClient({ enableAuth, enableCookies }?: {
32
+ initProviderClient({ enableAuth, enableCookies, rebuild }?: {
33
33
  enableAuth?: boolean;
34
34
  enableCookies?: boolean;
35
+ rebuild?: boolean;
35
36
  }): this;
36
37
  setupRuntimeConfig(config: RuntimeConfig): this;
37
38
  private queueInit;
@@ -104,10 +104,10 @@ export class AppContainer {
104
104
  this._configManager = configManager;
105
105
  });
106
106
  }
107
- initProviderClient({ enableAuth = true, enableCookies = true } = {}) {
107
+ initProviderClient({ enableAuth = true, enableCookies = true, rebuild = false } = {}) {
108
108
  this.initConfigManager();
109
109
  return this.queueInit('providerClient', async () => {
110
- if (this._providerClient) {
110
+ if (this._providerClient && !rebuild) {
111
111
  return;
112
112
  }
113
113
  await this.waitFor('configManager');
@@ -143,7 +143,7 @@ export class AppContainer {
143
143
  providerClient.use(authMiddleware);
144
144
  }
145
145
  this._providerClient = providerClient;
146
- });
146
+ }, rebuild);
147
147
  }
148
148
  setupRuntimeConfig(config) {
149
149
  if (this.initQueue.size > 0) {
@@ -160,8 +160,8 @@ export class AppContainer {
160
160
  }
161
161
  return this;
162
162
  }
163
- queueInit(name, task) {
164
- if (!this.initQueue.has(name)) {
163
+ queueInit(name, task, force = false) {
164
+ if (!this.initQueue.has(name) || force) {
165
165
  const promise = task();
166
166
  this.initQueue.set(name, promise);
167
167
  this.initPromises.push(promise);
@@ -1,4 +1,5 @@
1
1
  import { decode } from 'jsonwebtoken';
2
+ import { REFRESH_TOKEN_URI } from '../constants.js';
2
3
  import { AuthorizationRequiredError } from '../errors.js';
3
4
  function isAccessKeyExpired(accessKey) {
4
5
  try {
@@ -23,7 +24,7 @@ export async function createAuthMiddleware(configManager, providerClient, logger
23
24
  logger.info('Performed refresh token');
24
25
  let refreshResult;
25
26
  try {
26
- refreshResult = await providerClient.POST('/api/auth/refresh-access');
27
+ refreshResult = await providerClient.POST(REFRESH_TOKEN_URI);
27
28
  }
28
29
  catch (error) {
29
30
  logger.error({ err: error }, 'failure refresh token');
@@ -76,6 +77,9 @@ export async function createAuthMiddleware(configManager, providerClient, logger
76
77
  }
77
78
  const middleware = {
78
79
  async onRequest({ request }) {
80
+ if (request.url.endsWith(REFRESH_TOKEN_URI)) {
81
+ return request;
82
+ }
79
83
  const accessToken = await ensureAccessKey();
80
84
  if (accessToken) {
81
85
  request.headers.set('Authorization', `Bearer ${accessToken}`);
@@ -0,0 +1,34 @@
1
+ import { AddStorageDto, ProviderClient, StorageResponseDto, UpdateStorageDto } from '@super-protocol/provider-client';
2
+ import pino from 'pino';
3
+ import { IConfigManager } from '../interfaces/config-manager.interface.js';
4
+ export declare class StorageError extends Error {
5
+ }
6
+ export declare class StoragesEmptyError extends StorageError {
7
+ }
8
+ export declare class StorageCreateError extends StorageError {
9
+ }
10
+ export declare class StorageUpdateError extends StorageError {
11
+ }
12
+ export declare class StorageService {
13
+ private readonly configManager;
14
+ private readonly providerClient;
15
+ private readonly logger;
16
+ constructor(configManager: IConfigManager, providerClient: ProviderClient, logger: pino.BaseLogger);
17
+ createStorage(storage: AddStorageDto): Promise<StorageResponseDto>;
18
+ hasStorage(): Promise<boolean>;
19
+ initStorage(): Promise<{
20
+ bucket: string;
21
+ createdAt: string;
22
+ id: string;
23
+ isCentralized: boolean;
24
+ prefix: string;
25
+ s3Credentials?: import("@super-protocol/provider-client").components["schemas"]["S3CredentialsResponseDto"];
26
+ storageType: import("@super-protocol/provider-client").components["schemas"]["StorageType"];
27
+ storjCredentials?: import("@super-protocol/provider-client").components["schemas"]["StorJCredentialsResponseDto"];
28
+ updatedAt: string;
29
+ userId: string;
30
+ }>;
31
+ requestStorage(): Promise<StorageResponseDto[]>;
32
+ saveStorage(selectedStorage: string): Promise<void>;
33
+ updateStorage(id: string, storage: UpdateStorageDto): Promise<StorageResponseDto>;
34
+ }
@@ -0,0 +1,113 @@
1
+ import { ProviderClientError } from '../errors.js';
2
+ export class StorageError extends Error {
3
+ }
4
+ export class StoragesEmptyError extends StorageError {
5
+ }
6
+ export class StorageCreateError extends StorageError {
7
+ }
8
+ export class StorageUpdateError extends StorageError {
9
+ }
10
+ export class StorageService {
11
+ configManager;
12
+ providerClient;
13
+ logger;
14
+ constructor(configManager, providerClient, logger) {
15
+ this.configManager = configManager;
16
+ this.providerClient = providerClient;
17
+ this.logger = logger;
18
+ }
19
+ async createStorage(storage) {
20
+ this.logger.info({ storageType: storage.storageType }, 'Creating storage');
21
+ try {
22
+ const { data, error } = await this.providerClient.POST('/api/storages', {
23
+ body: storage,
24
+ });
25
+ if (error && error.message) {
26
+ this.logger.error({ err: error }, 'Failed to create storage');
27
+ throw new StorageCreateError(error.message);
28
+ }
29
+ if (!data) {
30
+ this.logger.error('Provider returned empty storage response');
31
+ throw new StorageCreateError('Incorrect response');
32
+ }
33
+ this.logger.info({ storageId: data.id }, 'Storage created successfully');
34
+ return data;
35
+ }
36
+ catch (error) {
37
+ this.logger.error({ err: error }, 'Storage creation failed');
38
+ const error_ = error instanceof StorageError ? error : new ProviderClientError('Request failed please try again later');
39
+ throw error_;
40
+ }
41
+ }
42
+ async hasStorage() {
43
+ return Boolean(await this.configManager.get('selectedStorage'));
44
+ }
45
+ async initStorage() {
46
+ this.logger.info('Requesting storage initialization');
47
+ try {
48
+ const result = await this.providerClient.POST('/api/storages/centralized');
49
+ if (result.error) {
50
+ this.logger.error({ err: result.error }, 'Failed to initialize storage');
51
+ throw new StorageCreateError(result.error.message);
52
+ }
53
+ this.logger.info('Storage initialized successfully');
54
+ return result.data;
55
+ }
56
+ catch (error) {
57
+ this.logger.error({ err: error }, 'Storage initialization failed');
58
+ const error_ = error instanceof StorageError ? error : new ProviderClientError('Request failed please try again later');
59
+ throw error_;
60
+ }
61
+ }
62
+ async requestStorage() {
63
+ let data;
64
+ this.logger.info('Requesting available storages');
65
+ try {
66
+ const result = await this.providerClient.GET('/api/storages');
67
+ data = result.data;
68
+ if (result.error) {
69
+ this.logger.error({ err: result.error }, 'Failed to fetch storages');
70
+ throw new StorageError(result.error.message);
71
+ }
72
+ if (!data || !data?.length) {
73
+ this.logger.warn('Storages list is empty');
74
+ throw new StoragesEmptyError('Storages is empty please create or import first one');
75
+ }
76
+ this.logger.debug({ count: data.length }, 'Received storages list');
77
+ return data;
78
+ }
79
+ catch (error) {
80
+ this.logger.error({ err: error }, 'Failed to request storages');
81
+ const error_ = error instanceof StorageError ? error : new ProviderClientError('Request failed please try again later');
82
+ throw error_;
83
+ }
84
+ }
85
+ async saveStorage(selectedStorage) {
86
+ this.logger.info({ selectedStorage }, 'Saving selected storage');
87
+ await this.configManager.set('selectedStorage', selectedStorage);
88
+ }
89
+ async updateStorage(id, storage) {
90
+ this.logger.info({ storageId: id }, 'Updating storage');
91
+ try {
92
+ const { data, error } = await this.providerClient.PATCH('/api/storages/{id}', {
93
+ body: storage,
94
+ params: { path: { id } },
95
+ });
96
+ if (error) {
97
+ this.logger.error({ err: error, storageId: id }, 'Failed to update storage');
98
+ throw new StorageUpdateError(error?.message);
99
+ }
100
+ if (!data) {
101
+ this.logger.error({ storageId: id }, 'Provider returned empty storage update response');
102
+ throw new StorageUpdateError('Incorrect response');
103
+ }
104
+ this.logger.info({ storageId: data.id }, 'Storage updated successfully');
105
+ return data;
106
+ }
107
+ catch (error) {
108
+ this.logger.error({ err: error, storageId: id }, 'Failed to update storage');
109
+ const error_ = error instanceof StorageError ? error : new ProviderClientError('Request failed please try again later');
110
+ throw error_;
111
+ }
112
+ }
113
+ }