@milaboratories/pl-client 2.4.10
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/README.md +52 -0
- package/dist/index.cjs +14527 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +14426 -0
- package/dist/index.js.map +1 -0
- package/package.json +49 -0
- package/src/core/auth.ts +27 -0
- package/src/core/client.test.ts +47 -0
- package/src/core/client.ts +302 -0
- package/src/core/config.test.ts +19 -0
- package/src/core/config.ts +197 -0
- package/src/core/default_client.ts +161 -0
- package/src/core/driver.ts +30 -0
- package/src/core/error.test.ts +14 -0
- package/src/core/errors.ts +84 -0
- package/src/core/http.ts +178 -0
- package/src/core/ll_client.test.ts +111 -0
- package/src/core/ll_client.ts +228 -0
- package/src/core/ll_transaction.test.ts +152 -0
- package/src/core/ll_transaction.ts +333 -0
- package/src/core/transaction.test.ts +173 -0
- package/src/core/transaction.ts +730 -0
- package/src/core/type_conversion.ts +121 -0
- package/src/core/types.test.ts +22 -0
- package/src/core/types.ts +223 -0
- package/src/core/unauth_client.test.ts +21 -0
- package/src/core/unauth_client.ts +48 -0
- package/src/helpers/pl.ts +141 -0
- package/src/helpers/poll.ts +178 -0
- package/src/helpers/rich_resource_types.test.ts +22 -0
- package/src/helpers/rich_resource_types.ts +84 -0
- package/src/helpers/smart_accessors.ts +146 -0
- package/src/helpers/state_helpers.ts +5 -0
- package/src/helpers/tx_helpers.ts +24 -0
- package/src/index.ts +14 -0
- package/src/proto/github.com/googleapis/googleapis/google/rpc/status.ts +125 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.client.ts +45 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/downloadapi/protocol.ts +271 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.client.ts +51 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/lsapi/protocol.ts +380 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.client.ts +59 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/progressapi/protocol.ts +450 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.client.ts +148 -0
- package/src/proto/github.com/milaboratory/pl/controllers/shared/grpc/streamingapi/protocol.ts +706 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/api.client.ts +406 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/api.ts +12636 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/api_types.ts +1384 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/base_types.ts +181 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/import.ts +251 -0
- package/src/proto/github.com/milaboratory/pl/plapi/plapiproto/resource_types.ts +693 -0
- package/src/proto/google/api/http.ts +687 -0
- package/src/proto/google/protobuf/any.ts +326 -0
- package/src/proto/google/protobuf/descriptor.ts +4502 -0
- package/src/proto/google/protobuf/duration.ts +230 -0
- package/src/proto/google/protobuf/empty.ts +81 -0
- package/src/proto/google/protobuf/struct.ts +482 -0
- package/src/proto/google/protobuf/timestamp.ts +287 -0
- package/src/proto/google/protobuf/wrappers.ts +751 -0
- package/src/test/test_config.test.ts +6 -0
- package/src/test/test_config.ts +166 -0
- package/src/util/branding.ts +4 -0
- package/src/util/pl.ts +11 -0
- package/src/util/util.test.ts +10 -0
- package/src/util/util.ts +9 -0
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
import fs from 'node:fs';
|
|
2
|
+
import { AuthInformation, plAddressToConfig, PlClientConfig } from './config';
|
|
3
|
+
import canonicalize from 'canonicalize';
|
|
4
|
+
import YAML from 'yaml';
|
|
5
|
+
import * as os from 'node:os';
|
|
6
|
+
import * as path from 'node:path';
|
|
7
|
+
import { notEmpty } from '@milaboratories/ts-helpers';
|
|
8
|
+
import { UnauthenticatedPlClient } from './unauth_client';
|
|
9
|
+
import { PlClient } from './client';
|
|
10
|
+
import { createHash } from 'crypto';
|
|
11
|
+
import { inferAuthRefreshTime } from './auth';
|
|
12
|
+
|
|
13
|
+
const CONFIG_FILE_LOCAL_JSON = 'pl.json';
|
|
14
|
+
const CONFIG_FILE_USER_JSON = path.join(os.homedir(), '.pl.json');
|
|
15
|
+
const CONFIG_FILE_LOCAL_YAML = 'pl.yaml';
|
|
16
|
+
const CONFIG_FILE_USER_YAML = path.join(os.homedir(), '.pl.yaml');
|
|
17
|
+
const CONF_FILE_SEQUENCE = [
|
|
18
|
+
CONFIG_FILE_LOCAL_JSON,
|
|
19
|
+
CONFIG_FILE_LOCAL_YAML,
|
|
20
|
+
CONFIG_FILE_USER_JSON,
|
|
21
|
+
CONFIG_FILE_USER_YAML
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
const AUTH_DATA_FILE = '.pl_auth.json';
|
|
25
|
+
|
|
26
|
+
type FileConfigOverrideFields =
|
|
27
|
+
| 'grpcProxy'
|
|
28
|
+
| 'httpProxy'
|
|
29
|
+
| 'user'
|
|
30
|
+
| 'password'
|
|
31
|
+
| 'alternativeRoot'
|
|
32
|
+
| 'defaultTransactionTimeout'
|
|
33
|
+
| 'defaultRequestTimeout'
|
|
34
|
+
| 'authTTLSeconds'
|
|
35
|
+
| 'authMaxRefreshSeconds';
|
|
36
|
+
const FILE_CONFIG_OVERRIDE_FIELDS: FileConfigOverrideFields[] = [
|
|
37
|
+
'grpcProxy',
|
|
38
|
+
'httpProxy',
|
|
39
|
+
'user',
|
|
40
|
+
'password',
|
|
41
|
+
'alternativeRoot',
|
|
42
|
+
'defaultTransactionTimeout',
|
|
43
|
+
'defaultRequestTimeout',
|
|
44
|
+
'authTTLSeconds',
|
|
45
|
+
'authMaxRefreshSeconds'
|
|
46
|
+
];
|
|
47
|
+
|
|
48
|
+
type PlConfigFile = {
|
|
49
|
+
address: string;
|
|
50
|
+
} & Partial<Pick<PlClientConfig, FileConfigOverrideFields>>;
|
|
51
|
+
|
|
52
|
+
interface AuthCache {
|
|
53
|
+
/** To check if config changed */
|
|
54
|
+
confHash: string;
|
|
55
|
+
expiration: number;
|
|
56
|
+
authInformation: AuthInformation;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function tryGetFileConfig(): [PlConfigFile, string] | undefined {
|
|
60
|
+
for (const confPath of CONF_FILE_SEQUENCE)
|
|
61
|
+
if (fs.existsSync(confPath)) {
|
|
62
|
+
const fileContent = fs.readFileSync(confPath, { encoding: 'utf-8' });
|
|
63
|
+
if (confPath.endsWith('json')) return [JSON.parse(fileContent) as PlConfigFile, confPath];
|
|
64
|
+
else return [YAML.parse(fileContent) as PlConfigFile, confPath];
|
|
65
|
+
}
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function saveAuthInfoCallback(
|
|
70
|
+
confHash: string,
|
|
71
|
+
authMaxRefreshSeconds: number
|
|
72
|
+
): (newAuthInfo: AuthInformation) => void {
|
|
73
|
+
return (newAuthInfo) => {
|
|
74
|
+
fs.writeFileSync(
|
|
75
|
+
AUTH_DATA_FILE,
|
|
76
|
+
Buffer.from(
|
|
77
|
+
JSON.stringify({
|
|
78
|
+
confHash,
|
|
79
|
+
authInformation: newAuthInfo,
|
|
80
|
+
expiration: inferAuthRefreshTime(newAuthInfo, authMaxRefreshSeconds)
|
|
81
|
+
} as AuthCache)
|
|
82
|
+
),
|
|
83
|
+
'utf8'
|
|
84
|
+
);
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const cleanAuthInfoCallback = () => {
|
|
89
|
+
fs.rmSync(AUTH_DATA_FILE);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
/** Uses default algorithm to construct a pl client from the environment */
|
|
93
|
+
export async function defaultPlClient(): Promise<PlClient> {
|
|
94
|
+
let config: PlClientConfig | undefined = undefined;
|
|
95
|
+
if (process.env.PL_ADDRESS !== undefined) {
|
|
96
|
+
config = plAddressToConfig(process.env.PL_ADDRESS);
|
|
97
|
+
} else {
|
|
98
|
+
const fromFile = tryGetFileConfig();
|
|
99
|
+
if (fromFile !== undefined) {
|
|
100
|
+
const [fileConfig, configPath] = fromFile;
|
|
101
|
+
const address = notEmpty(fileConfig.address, `no pl address in file: ${configPath}`);
|
|
102
|
+
config = plAddressToConfig(address);
|
|
103
|
+
// applying overrides
|
|
104
|
+
for (const field of FILE_CONFIG_OVERRIDE_FIELDS)
|
|
105
|
+
if (fileConfig[field] !== undefined) (config as any)[field] = fileConfig[field];
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (config === undefined)
|
|
110
|
+
throw new Error("Can't find configuration to create default platform client.");
|
|
111
|
+
|
|
112
|
+
if (process.env.PL_USER !== undefined) config.user = process.env.PL_USER;
|
|
113
|
+
|
|
114
|
+
if (process.env.PL_PASSWORD !== undefined) config.user = process.env.PL_PASSWORD;
|
|
115
|
+
|
|
116
|
+
const confHash = createHash('sha256')
|
|
117
|
+
.update(Buffer.from(canonicalize(config)!))
|
|
118
|
+
.digest('base64');
|
|
119
|
+
|
|
120
|
+
let authInformation: AuthInformation | undefined = undefined;
|
|
121
|
+
|
|
122
|
+
// try recover auth information from cache
|
|
123
|
+
if (fs.existsSync(AUTH_DATA_FILE)) {
|
|
124
|
+
const cache: AuthCache = JSON.parse(fs.readFileSync(AUTH_DATA_FILE, { encoding: 'utf-8' }));
|
|
125
|
+
if (cache.confHash === confHash && cache.expiration > Date.now())
|
|
126
|
+
authInformation = cache.authInformation;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (authInformation === undefined) {
|
|
130
|
+
const client = new UnauthenticatedPlClient(config);
|
|
131
|
+
|
|
132
|
+
if (await client.requireAuth()) {
|
|
133
|
+
if (config.user === undefined || config.password === undefined)
|
|
134
|
+
throw new Error(`No auth information for found to authenticate with PL server.`);
|
|
135
|
+
authInformation = await client.login(config.user, config.password);
|
|
136
|
+
} else {
|
|
137
|
+
// No authorization is required
|
|
138
|
+
authInformation = {};
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// saving cache
|
|
142
|
+
fs.writeFileSync(
|
|
143
|
+
AUTH_DATA_FILE,
|
|
144
|
+
Buffer.from(
|
|
145
|
+
JSON.stringify({
|
|
146
|
+
confHash,
|
|
147
|
+
authInformation,
|
|
148
|
+
expiration: inferAuthRefreshTime(authInformation, config.authMaxRefreshSeconds)
|
|
149
|
+
} as AuthCache)
|
|
150
|
+
),
|
|
151
|
+
'utf8'
|
|
152
|
+
);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
return await PlClient.init(config, {
|
|
156
|
+
authInformation,
|
|
157
|
+
onUpdate: (newAuthInfo) => saveAuthInfoCallback(confHash, config!.authMaxRefreshSeconds),
|
|
158
|
+
onUpdateError: cleanAuthInfoCallback,
|
|
159
|
+
onAuthError: cleanAuthInfoCallback
|
|
160
|
+
});
|
|
161
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { PlClient } from './client';
|
|
2
|
+
import { GrpcTransport } from '@protobuf-ts/grpc-transport';
|
|
3
|
+
import type { RpcOptions } from '@protobuf-ts/runtime-rpc';
|
|
4
|
+
import { Dispatcher } from 'undici';
|
|
5
|
+
import { ResourceType } from './types';
|
|
6
|
+
|
|
7
|
+
/** Drivers must implement this interface */
|
|
8
|
+
export interface PlDriver {
|
|
9
|
+
close(): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/** Definition to use driver via {@link PlClient} */
|
|
13
|
+
export interface PlDriverDefinition<Drv extends PlDriver> {
|
|
14
|
+
/** Used as key to only once instantiate specific drivers */
|
|
15
|
+
readonly name: string;
|
|
16
|
+
|
|
17
|
+
/** Initialization routine, will be executed only once for each driver in a specific client */
|
|
18
|
+
init(pl: PlClient, grpcTransport: GrpcTransport, httpDispatcher: Dispatcher): Drv;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// addRTypeToMetadata adds a metadata with resource type
|
|
22
|
+
// for every RPC call. It is necessary for the platform core
|
|
23
|
+
// to proxy the call to the proper controller.
|
|
24
|
+
export function addRTypeToMetadata(rType: ResourceType, options?: RpcOptions) {
|
|
25
|
+
options = options ?? {};
|
|
26
|
+
options.meta = options.meta ?? {};
|
|
27
|
+
options.meta['resourceType'] = `${rType.name}:${rType.version}`;
|
|
28
|
+
|
|
29
|
+
return options;
|
|
30
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import * as tp from 'node:timers/promises';
|
|
2
|
+
import { isTimeoutOrCancelError } from './errors';
|
|
3
|
+
|
|
4
|
+
test('timeout of sleep error type detection', async () => {
|
|
5
|
+
let noError = false;
|
|
6
|
+
try {
|
|
7
|
+
await tp.setTimeout(1000, undefined, { signal: AbortSignal.timeout(10) });
|
|
8
|
+
noError = true;
|
|
9
|
+
} catch (err: unknown) {
|
|
10
|
+
expect((err as any).code).toStrictEqual('ABORT_ERR');
|
|
11
|
+
expect(isTimeoutOrCancelError(err)).toEqual(true);
|
|
12
|
+
}
|
|
13
|
+
expect(noError).toBe(false);
|
|
14
|
+
});
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { Status } from '../proto/github.com/googleapis/googleapis/google/rpc/status';
|
|
2
|
+
import { Aborted } from '@milaboratories/ts-helpers';
|
|
3
|
+
|
|
4
|
+
export function isConnectionProblem(err: unknown, nested: boolean = false): boolean {
|
|
5
|
+
if (err instanceof DisconnectedError) return true;
|
|
6
|
+
if ((err as any).name == 'RpcError' && (err as any).code == 'UNAVAILABLE') return true;
|
|
7
|
+
if ((err as any).cause !== undefined && !nested)
|
|
8
|
+
// nested limits the depth of search
|
|
9
|
+
return isConnectionProblem((err as any).cause, true);
|
|
10
|
+
return false;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export function isUnauthenticated(err: unknown, nested: boolean = false): boolean {
|
|
14
|
+
if (err instanceof UnauthenticatedError) return true;
|
|
15
|
+
if ((err as any).name == 'RpcError' && (err as any).code == 'UNAUTHENTICATED') return true;
|
|
16
|
+
if ((err as any).cause !== undefined && !nested)
|
|
17
|
+
// nested limits the depth of search
|
|
18
|
+
return isUnauthenticated((err as any).cause, true);
|
|
19
|
+
return false;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isTimeoutOrCancelError(err: unknown, nested: boolean = false): boolean {
|
|
23
|
+
if (err instanceof Aborted || (err as any).name == 'AbortError') return true;
|
|
24
|
+
if ((err as any).code == 'ABORT_ERR') return true;
|
|
25
|
+
if (
|
|
26
|
+
(err as any).name == 'RpcError' &&
|
|
27
|
+
((err as any).code == 'CANCELLED' || (err as any).code == 'DEADLINE_EXCEEDED')
|
|
28
|
+
)
|
|
29
|
+
return true;
|
|
30
|
+
if ((err as any).cause !== undefined && !nested)
|
|
31
|
+
// nested limits the depth of search
|
|
32
|
+
return isTimeoutOrCancelError((err as any).cause, true);
|
|
33
|
+
return false;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const PlErrorCodeNotFound = 5;
|
|
37
|
+
|
|
38
|
+
export class PlError extends Error {
|
|
39
|
+
constructor(public readonly status: Status) {
|
|
40
|
+
super(`code=${status.code} ${status.message}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export function throwPlNotFoundError(message: string): never {
|
|
45
|
+
throw new RecoverablePlError({ code: PlErrorCodeNotFound, message, details: [] });
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export class RecoverablePlError extends PlError {
|
|
49
|
+
constructor(status: Status) {
|
|
50
|
+
super(status);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class UnrecoverablePlError extends PlError {
|
|
55
|
+
constructor(status: Status) {
|
|
56
|
+
super(status);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function isNotFoundError(err: unknown, nested: boolean = false): boolean {
|
|
61
|
+
if ((err as any).name == 'RpcError' && (err as any).code == 'NOT_FOUND') return true;
|
|
62
|
+
if ((err as any).cause !== undefined && !nested) return isNotFoundError((err as any).cause, true);
|
|
63
|
+
return err instanceof RecoverablePlError && err.status.code === PlErrorCodeNotFound;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export class UnauthenticatedError extends Error {
|
|
67
|
+
constructor(message: string) {
|
|
68
|
+
super('LoginFailed: ' + message);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export class DisconnectedError extends Error {
|
|
73
|
+
constructor(message: string) {
|
|
74
|
+
super('Disconnected: ' + message);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function rethrowMeaningfulError(error: any, wrapIfUnknown: boolean = false): never {
|
|
79
|
+
if (isUnauthenticated(error)) throw new UnauthenticatedError(error.message);
|
|
80
|
+
if (isConnectionProblem(error)) throw new DisconnectedError(error.message);
|
|
81
|
+
if (isTimeoutOrCancelError(error)) throw new Aborted(error);
|
|
82
|
+
if (wrapIfUnknown) throw new Error(error.message, { cause: error });
|
|
83
|
+
else throw error;
|
|
84
|
+
}
|
package/src/core/http.ts
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
// import * as util from 'util';
|
|
2
|
+
// import http from 'node:http';
|
|
3
|
+
// import https from 'node:https';
|
|
4
|
+
// import { Readable, finished } from 'stream';
|
|
5
|
+
//
|
|
6
|
+
// type RequestOptions = https.RequestOptions;
|
|
7
|
+
//
|
|
8
|
+
// export const finishedP = util.promisify(finished);
|
|
9
|
+
//
|
|
10
|
+
// export const readableToBuffer = (
|
|
11
|
+
// stream: http.IncomingMessage
|
|
12
|
+
// ): Promise<Buffer> => {
|
|
13
|
+
// if (stream.destroyed) {
|
|
14
|
+
// return Promise.reject(stream.errored);
|
|
15
|
+
// }
|
|
16
|
+
//
|
|
17
|
+
// return new Promise((resolve, reject) => {
|
|
18
|
+
// const chunks: Buffer[] = [];
|
|
19
|
+
// stream.on('data', chunk => chunks.push(chunk));
|
|
20
|
+
// stream.on('error', reject);
|
|
21
|
+
// stream.on('end', () => {
|
|
22
|
+
// resolve(Buffer.concat(chunks));
|
|
23
|
+
// });
|
|
24
|
+
// });
|
|
25
|
+
// };
|
|
26
|
+
//
|
|
27
|
+
// class Body {
|
|
28
|
+
// private buff: Buffer | undefined;
|
|
29
|
+
//
|
|
30
|
+
// public constructor(private message: http.IncomingMessage) {}
|
|
31
|
+
//
|
|
32
|
+
// async redable() {
|
|
33
|
+
// return this.buffer().then(b => Readable.from(b));
|
|
34
|
+
// }
|
|
35
|
+
//
|
|
36
|
+
// async buffer(): Promise<Buffer> {
|
|
37
|
+
// if (!this.buff) {
|
|
38
|
+
// this.buff = await readableToBuffer(this.message).catch(e => {
|
|
39
|
+
// throw e;
|
|
40
|
+
// });
|
|
41
|
+
// }
|
|
42
|
+
//
|
|
43
|
+
// return this.buff;
|
|
44
|
+
// }
|
|
45
|
+
//
|
|
46
|
+
// async string() {
|
|
47
|
+
// return this.buffer().then(b => b.toString());
|
|
48
|
+
// }
|
|
49
|
+
//
|
|
50
|
+
// async json() {
|
|
51
|
+
// return this.string().then(s => JSON.parse(s));
|
|
52
|
+
// }
|
|
53
|
+
// }
|
|
54
|
+
//
|
|
55
|
+
// export class HttpClient {
|
|
56
|
+
// constructor(private options: RequestOptions) {
|
|
57
|
+
// }
|
|
58
|
+
//
|
|
59
|
+
// assingOptions(options: RequestOptions) {
|
|
60
|
+
// Object.assign(this.options, options);
|
|
61
|
+
// }
|
|
62
|
+
//
|
|
63
|
+
// request(
|
|
64
|
+
// url: URL | string,
|
|
65
|
+
// conf: {
|
|
66
|
+
// data?: Buffer | string;
|
|
67
|
+
// } & RequestOptions
|
|
68
|
+
// ) {
|
|
69
|
+
// url = typeof url === 'string' ? new URL(url) : url;
|
|
70
|
+
//
|
|
71
|
+
// const data = conf.data ?? Buffer.from('');
|
|
72
|
+
//
|
|
73
|
+
// delete conf.data;
|
|
74
|
+
//
|
|
75
|
+
// const options: RequestOptions = Object.assign(
|
|
76
|
+
// {
|
|
77
|
+
// hostname: url.hostname,
|
|
78
|
+
// path: url.pathname + url.search,
|
|
79
|
+
// port: url.port,
|
|
80
|
+
// } as RequestOptions,
|
|
81
|
+
// this.options,
|
|
82
|
+
// conf,
|
|
83
|
+
// );
|
|
84
|
+
//
|
|
85
|
+
// if (!options.method) {
|
|
86
|
+
// options.method = 'GET';
|
|
87
|
+
// }
|
|
88
|
+
//
|
|
89
|
+
// if (!options.headers) {
|
|
90
|
+
// options.headers = {};
|
|
91
|
+
// }
|
|
92
|
+
//
|
|
93
|
+
// options.headers['Content-Length'] = Buffer.byteLength(data);
|
|
94
|
+
//
|
|
95
|
+
// // console.log('options', JSON.stringify(options, null, 2));
|
|
96
|
+
//
|
|
97
|
+
// const isTls = url.protocol === 'https:';
|
|
98
|
+
//
|
|
99
|
+
// const lib = isTls ? https : http;
|
|
100
|
+
//
|
|
101
|
+
// return new Promise<{
|
|
102
|
+
// statusCode: number;
|
|
103
|
+
// ok: boolean;
|
|
104
|
+
// headers: http.IncomingHttpHeaders;
|
|
105
|
+
// body: Body;
|
|
106
|
+
// }>((resolve, reject) => {
|
|
107
|
+
// const timeout = options.timeout ?? 30000;
|
|
108
|
+
//
|
|
109
|
+
// const t = setTimeout(() => {
|
|
110
|
+
// req.end();
|
|
111
|
+
// reject(Error(`Timeout ${timeout} exceeded`));
|
|
112
|
+
// }, timeout);
|
|
113
|
+
//
|
|
114
|
+
// resolve = wrapFunction(resolve, () => clearTimeout(t)); // @TODO PromiseTimeout in utils
|
|
115
|
+
// reject = wrapFunction(reject, () => clearTimeout(t));
|
|
116
|
+
//
|
|
117
|
+
// const req = lib.request(options, message => {
|
|
118
|
+
// const statusCode = message.statusCode ?? 500;
|
|
119
|
+
// resolve({
|
|
120
|
+
// statusCode,
|
|
121
|
+
// headers: message.headers,
|
|
122
|
+
// ok: statusCode >= 200 && statusCode < 300,
|
|
123
|
+
// body: new Body(message),
|
|
124
|
+
// });
|
|
125
|
+
// });
|
|
126
|
+
//
|
|
127
|
+
// req.on('connect', (message, socket) => {
|
|
128
|
+
// const statusCode = message.statusCode ?? 500;
|
|
129
|
+
// socket.end();
|
|
130
|
+
// socket.on('error', () => {});
|
|
131
|
+
// resolve({
|
|
132
|
+
// statusCode,
|
|
133
|
+
// ok: statusCode >= 200 && statusCode < 300,
|
|
134
|
+
// headers: message.headers,
|
|
135
|
+
// body: new Body(message),
|
|
136
|
+
// });
|
|
137
|
+
// });
|
|
138
|
+
//
|
|
139
|
+
// req.on('error', e => {
|
|
140
|
+
// reject(e);
|
|
141
|
+
// });
|
|
142
|
+
//
|
|
143
|
+
// if (data.length) {
|
|
144
|
+
// req.write(data);
|
|
145
|
+
// }
|
|
146
|
+
//
|
|
147
|
+
// req.end();
|
|
148
|
+
// });
|
|
149
|
+
// }
|
|
150
|
+
//
|
|
151
|
+
// async connectProxy(proxy: URL | string) {
|
|
152
|
+
// proxy = new URL(proxy);
|
|
153
|
+
//
|
|
154
|
+
// const headers = {} as http.OutgoingHttpHeaders;
|
|
155
|
+
//
|
|
156
|
+
// if (proxy.username && proxy.password) {
|
|
157
|
+
// headers['Proxy-Authorization'] = `Basic ${Buffer.from(proxy.username + ':' + proxy.password).toString('base64')}`;
|
|
158
|
+
// }
|
|
159
|
+
//
|
|
160
|
+
// return this.request(proxy, {
|
|
161
|
+
// method: 'CONNECT',
|
|
162
|
+
// path: 'www.google.com:80', // @TODO temp (it seems that we need a target in order to get 200 from the proxy)
|
|
163
|
+
// headers,
|
|
164
|
+
// agent: undefined
|
|
165
|
+
// }).then(r => ({ok: r.ok, statusCode: r.statusCode})).catch(() => ({ok: false, statusCode: 500}));
|
|
166
|
+
// }
|
|
167
|
+
// }
|
|
168
|
+
//
|
|
169
|
+
// export const wrapFunction = <T extends unknown[], U>(
|
|
170
|
+
// fn: (...args: T) => U,
|
|
171
|
+
// before: () => void
|
|
172
|
+
// ) => {
|
|
173
|
+
// return (...args: T): U => {
|
|
174
|
+
// before();
|
|
175
|
+
// return fn(...args);
|
|
176
|
+
// };
|
|
177
|
+
// };
|
|
178
|
+
//
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { LLPlClient } from './ll_client';
|
|
2
|
+
import { getTestConfig, getTestLLClient, getTestClientConf } from '../test/test_config';
|
|
3
|
+
import { TxAPI_Open_Request_WritableTx } from '../proto/github.com/milaboratory/pl/plapi/plapiproto/api';
|
|
4
|
+
import { request } from 'undici';
|
|
5
|
+
import * as tp from 'node:timers/promises';
|
|
6
|
+
|
|
7
|
+
import { UnauthenticatedError } from './errors';
|
|
8
|
+
|
|
9
|
+
test('authenticated instance test', async () => {
|
|
10
|
+
const client = await getTestLLClient();
|
|
11
|
+
const tx = client.createTx();
|
|
12
|
+
const response = await tx.send(
|
|
13
|
+
{
|
|
14
|
+
oneofKind: 'txOpen',
|
|
15
|
+
txOpen: { name: 'test', writable: TxAPI_Open_Request_WritableTx.WRITABLE }
|
|
16
|
+
},
|
|
17
|
+
false
|
|
18
|
+
);
|
|
19
|
+
expect(response.txOpen.tx?.isValid).toBeTruthy();
|
|
20
|
+
await tx.complete();
|
|
21
|
+
await tx.await();
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
test('unauthenticated status change', async () => {
|
|
25
|
+
const cfg = getTestConfig();
|
|
26
|
+
if (cfg.test_password === undefined) {
|
|
27
|
+
console.log("skipping test because target server doesn't support authentication");
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const client = new LLPlClient(cfg.address);
|
|
32
|
+
expect(client.status).toBe('OK');
|
|
33
|
+
|
|
34
|
+
const tx = client.createTx();
|
|
35
|
+
|
|
36
|
+
await expect(async () => {
|
|
37
|
+
await tx.send(
|
|
38
|
+
{
|
|
39
|
+
oneofKind: 'txOpen',
|
|
40
|
+
txOpen: { name: 'test', writable: TxAPI_Open_Request_WritableTx.WRITABLE }
|
|
41
|
+
},
|
|
42
|
+
false
|
|
43
|
+
);
|
|
44
|
+
}).rejects.toThrow(UnauthenticatedError);
|
|
45
|
+
|
|
46
|
+
await expect(async () => {
|
|
47
|
+
await tx.await();
|
|
48
|
+
}).rejects.toThrow(UnauthenticatedError);
|
|
49
|
+
|
|
50
|
+
await tp.setImmediate();
|
|
51
|
+
|
|
52
|
+
expect(client.status).toEqual('Unauthenticated');
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test('automatic token update', async () => {
|
|
56
|
+
const { conf, auth } = await getTestClientConf();
|
|
57
|
+
conf.authMaxRefreshSeconds = 1;
|
|
58
|
+
let numberOfAuthUpdates = 0;
|
|
59
|
+
const client = new LLPlClient(conf, {
|
|
60
|
+
auth: {
|
|
61
|
+
authInformation: auth.authInformation,
|
|
62
|
+
onUpdate: (auth) => {
|
|
63
|
+
console.log(auth);
|
|
64
|
+
++numberOfAuthUpdates;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
for (let i = 0; i < 6; i++) {
|
|
70
|
+
const tx = client.createTx();
|
|
71
|
+
const response = await tx.send(
|
|
72
|
+
{
|
|
73
|
+
oneofKind: 'txOpen',
|
|
74
|
+
txOpen: { name: 'test', writable: TxAPI_Open_Request_WritableTx.WRITABLE }
|
|
75
|
+
},
|
|
76
|
+
false
|
|
77
|
+
);
|
|
78
|
+
expect(response.txOpen.tx?.isValid).toBeTruthy();
|
|
79
|
+
await tx.complete();
|
|
80
|
+
await tx.await();
|
|
81
|
+
|
|
82
|
+
if (numberOfAuthUpdates > 1) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
await tp.setTimeout(1000);
|
|
87
|
+
}
|
|
88
|
+
}, 5000);
|
|
89
|
+
|
|
90
|
+
test('test simple https call', async () => {
|
|
91
|
+
const client = await getTestLLClient();
|
|
92
|
+
const response = await request('https://cdn.milaboratory.com/ping', {
|
|
93
|
+
dispatcher: client.httpDispatcher
|
|
94
|
+
});
|
|
95
|
+
const text = await response.body.text();
|
|
96
|
+
expect(text).toEqual('pong');
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('test https call via proxy', async () => {
|
|
100
|
+
const testConfig = getTestConfig();
|
|
101
|
+
if (testConfig.test_proxy === undefined) {
|
|
102
|
+
console.log('skipped');
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const client = await getTestLLClient({ httpProxy: testConfig.test_proxy });
|
|
106
|
+
const response = await request('https://cdn.milaboratory.com/ping', {
|
|
107
|
+
dispatcher: client.httpDispatcher
|
|
108
|
+
});
|
|
109
|
+
const text = await response.body.text();
|
|
110
|
+
expect(text).toEqual('pong');
|
|
111
|
+
});
|