@bitblit/ratchet-common 6.0.146-alpha → 6.0.148-alpha
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/package.json +2 -1
- package/src/2d/line-2d.ts +6 -0
- package/src/2d/matrix-factory.ts +94 -0
- package/src/2d/plane-2d-type.ts +6 -0
- package/src/2d/plane-2d.ts +7 -0
- package/src/2d/point-2d.ts +4 -0
- package/src/2d/poly-line-2d.ts +5 -0
- package/src/2d/ratchet-2d.spec.ts +205 -0
- package/src/2d/ratchet-2d.ts +350 -0
- package/src/2d/transformation-matrix.ts +19 -0
- package/src/build/build-information.ts +8 -0
- package/src/build/ratchet-common-info.ts +19 -0
- package/src/histogram/histogram-entry.ts +4 -0
- package/src/histogram/histogram.spec.ts +25 -0
- package/src/histogram/histogram.ts +61 -0
- package/src/jwt/common-jwt-token.ts +17 -0
- package/src/jwt/expired-jwt-handling.ts +5 -0
- package/src/jwt/jwt-decode-only-ratchet.ts +26 -0
- package/src/jwt/jwt-payload-expiration-ratchet.ts +45 -0
- package/src/jwt/jwt-token-base.ts +14 -0
- package/src/lang/array-ratchet.spec.ts +79 -0
- package/src/lang/array-ratchet.ts +141 -0
- package/src/lang/base64-ratchet.spec.ts +48 -0
- package/src/lang/base64-ratchet.ts +247 -0
- package/src/lang/boolean-ratchet.spec.ts +95 -0
- package/src/lang/boolean-ratchet.ts +52 -0
- package/src/lang/composite-last-success-provider.spec.ts +31 -0
- package/src/lang/composite-last-success-provider.ts +30 -0
- package/src/lang/currency-ratchet.ts +29 -0
- package/src/lang/date-ratchet.spec.ts +27 -0
- package/src/lang/date-ratchet.ts +42 -0
- package/src/lang/duration-ratchet.spec.ts +47 -0
- package/src/lang/duration-ratchet.ts +77 -0
- package/src/lang/enum-ratchet.spec.ts +45 -0
- package/src/lang/enum-ratchet.ts +41 -0
- package/src/lang/error-handling-approach.ts +6 -0
- package/src/lang/error-ratchet.spec.ts +25 -0
- package/src/lang/error-ratchet.ts +70 -0
- package/src/lang/esm-ratchet.ts +81 -0
- package/src/lang/expiring-object.spec.ts +56 -0
- package/src/lang/expiring-object.ts +84 -0
- package/src/lang/geolocation-ratchet.spec.ts +177 -0
- package/src/lang/geolocation-ratchet.ts +341 -0
- package/src/lang/global-ratchet.spec.ts +17 -0
- package/src/lang/global-ratchet.ts +105 -0
- package/src/lang/key-value.ts +8 -0
- package/src/lang/last-success-provider.ts +4 -0
- package/src/lang/map-ratchet.spec.ts +113 -0
- package/src/lang/map-ratchet.ts +220 -0
- package/src/lang/no.spec.ts +9 -0
- package/src/lang/no.ts +7 -0
- package/src/lang/number-ratchet.spec.ts +154 -0
- package/src/lang/number-ratchet.ts +253 -0
- package/src/lang/parsed-url.ts +10 -0
- package/src/lang/promise-ratchet.spec.ts +104 -0
- package/src/lang/promise-ratchet.ts +196 -0
- package/src/lang/range.ts +4 -0
- package/src/lang/require-ratchet.spec.ts +85 -0
- package/src/lang/require-ratchet.ts +68 -0
- package/src/lang/simple-arg-ratchet.spec.ts +13 -0
- package/src/lang/simple-arg-ratchet.ts +47 -0
- package/src/lang/simple-encryption-ratchet.ts +88 -0
- package/src/lang/sort-ratchet.spec.ts +58 -0
- package/src/lang/sort-ratchet.ts +50 -0
- package/src/lang/stop-watch.spec.ts +53 -0
- package/src/lang/stop-watch.ts +202 -0
- package/src/lang/string-ratchet.spec.ts +226 -0
- package/src/lang/string-ratchet.ts +676 -0
- package/src/lang/time-zone-ratchet.spec.ts +51 -0
- package/src/lang/time-zone-ratchet.ts +148 -0
- package/src/lang/timeout-token.spec.ts +12 -0
- package/src/lang/timeout-token.ts +21 -0
- package/src/lang/uint-8-array-ratchet.spec.ts +22 -0
- package/src/lang/uint-8-array-ratchet.ts +48 -0
- package/src/lang/web-stream-ratchet.spec.ts +12 -0
- package/src/lang/web-stream-ratchet.ts +96 -0
- package/src/logger/classic-single-line-log-message-formatter.ts +19 -0
- package/src/logger/log-message-builder.ts +60 -0
- package/src/logger/log-message-format-type.ts +11 -0
- package/src/logger/log-message-formatter.ts +6 -0
- package/src/logger/log-message-processor.ts +6 -0
- package/src/logger/log-message.ts +9 -0
- package/src/logger/log-snapshot.ts +6 -0
- package/src/logger/logger-instance.ts +269 -0
- package/src/logger/logger-level-name.ts +11 -0
- package/src/logger/logger-meta.ts +7 -0
- package/src/logger/logger-options.ts +14 -0
- package/src/logger/logger-output-function.ts +10 -0
- package/src/logger/logger-ring-buffer.ts +89 -0
- package/src/logger/logger-util.spec.ts +11 -0
- package/src/logger/logger-util.ts +68 -0
- package/src/logger/logger.spec.ts +177 -0
- package/src/logger/logger.ts +213 -0
- package/src/logger/none-log-message-formatter.ts +10 -0
- package/src/logger/single-line-no-level-log-message-formatter.ts +18 -0
- package/src/logger/structured-json-log-message-formatter.ts +25 -0
- package/src/mail/archive-email-result.ts +8 -0
- package/src/mail/email-attachment.ts +23 -0
- package/src/mail/mail-sending-provider.ts +21 -0
- package/src/mail/mailer-config.ts +30 -0
- package/src/mail/mailer-like.ts +38 -0
- package/src/mail/mailer-util.ts +65 -0
- package/src/mail/mailer.spec.ts +120 -0
- package/src/mail/mailer.ts +214 -0
- package/src/mail/ready-to-send-email.ts +67 -0
- package/src/mail/resolved-ready-to-send-email.ts +17 -0
- package/src/mail/send-email-result.ts +16 -0
- package/src/mail/test-mail-sending-provider.ts +35 -0
- package/src/network/browser-local-ip-provider.spec.ts +23 -0
- package/src/network/browser-local-ip-provider.ts +26 -0
- package/src/network/fixed-local-ip-provider.ts +9 -0
- package/src/network/local-ip-provider.ts +4 -0
- package/src/network/network-ratchet.spec.ts +17 -0
- package/src/network/network-ratchet.ts +209 -0
- package/src/network/remote-file-tracker/backup-result.ts +6 -0
- package/src/network/remote-file-tracker/file-transfer-result-type.ts +5 -0
- package/src/network/remote-file-tracker/file-transfer-result.ts +9 -0
- package/src/network/remote-file-tracker/remote-file-tracker-options.ts +6 -0
- package/src/network/remote-file-tracker/remote-file-tracker-push-options.ts +4 -0
- package/src/network/remote-file-tracker/remote-file-tracker.ts +117 -0
- package/src/network/remote-file-tracker/remote-file-tracking-provider.ts +19 -0
- package/src/network/remote-file-tracker/remote-status-data-and-content.ts +6 -0
- package/src/network/remote-file-tracker/remote-status-data.ts +7 -0
- package/src/network/restful-api-http-error.spec.ts +13 -0
- package/src/network/restful-api-http-error.ts +173 -0
- package/src/template/ratchet-template-renderer.ts +8 -0
- package/src/third-party/google/google-recaptcha-ratchet.spec.ts +27 -0
- package/src/third-party/google/google-recaptcha-ratchet.ts +36 -0
- package/src/third-party/twilio/twilio-ratchet.ts +92 -0
- package/src/third-party/twilio/twilio-verify-ratchet.ts +83 -0
- package/src/transform/built-in-transforms.ts +214 -0
- package/src/transform/transform-ratchet.spec.ts +134 -0
- package/src/transform/transform-ratchet.ts +88 -0
- package/src/transform/transform-rule.ts +7 -0
- package/src/tx/transaction-configuration.ts +8 -0
- package/src/tx/transaction-final-state.ts +7 -0
- package/src/tx/transaction-ratchet.spec.ts +150 -0
- package/src/tx/transaction-ratchet.ts +98 -0
- package/src/tx/transaction-result.ts +10 -0
- package/src/tx/transaction-step.ts +5 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
import { RemoteStatusData } from './remote-status-data.js';
|
|
2
|
+
import { RemoteStatusDataAndContent } from './remote-status-data-and-content.js';
|
|
3
|
+
import { FileTransferResultType } from './file-transfer-result-type.js';
|
|
4
|
+
import { FileTransferResult } from './file-transfer-result.js';
|
|
5
|
+
import { Logger } from '../../logger/logger.js';
|
|
6
|
+
import { RemoteFileTrackerOptions } from './remote-file-tracker-options.js';
|
|
7
|
+
import { RequireRatchet } from '../../lang/require-ratchet.js';
|
|
8
|
+
import { RemoteFileTrackerPushOptions } from './remote-file-tracker-push-options.js';
|
|
9
|
+
import { WebStreamRatchet } from '../../lang/web-stream-ratchet.js';
|
|
10
|
+
|
|
11
|
+
export class RemoteFileTracker<KeyType> {
|
|
12
|
+
// Updated every type you sync or pull
|
|
13
|
+
private _remoteStatusData: RemoteStatusData<KeyType>;
|
|
14
|
+
|
|
15
|
+
constructor(private opts: RemoteFileTrackerOptions<KeyType>) {
|
|
16
|
+
RequireRatchet.notNullOrUndefined(opts, 'opts');
|
|
17
|
+
RequireRatchet.notNullOrUndefined(opts.provider, 'opts.provider');
|
|
18
|
+
RequireRatchet.notNullOrUndefined(opts.key, 'opts.key');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public get remoteStatusData(): RemoteStatusData<KeyType> {
|
|
22
|
+
// Gen a copy to prevent accidental corruption
|
|
23
|
+
return Object.assign({}, this._remoteStatusData);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// Rereads the remote side
|
|
27
|
+
public async sync(): Promise<RemoteStatusData<KeyType>> {
|
|
28
|
+
this._remoteStatusData = await this.opts.provider.readRemoteStatus(this.opts.key);
|
|
29
|
+
return this._remoteStatusData;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async pullRemoteData(ifNewerThan?: RemoteStatusData<KeyType>): Promise<RemoteStatusDataAndContent<KeyType>> {
|
|
33
|
+
const rval: RemoteStatusDataAndContent<KeyType> = await this.opts.provider.pullRemoteData(this.opts.key, ifNewerThan);
|
|
34
|
+
if (rval) {
|
|
35
|
+
this._remoteStatusData = rval.status;
|
|
36
|
+
}
|
|
37
|
+
return rval;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public static async dataAsString(data: RemoteStatusDataAndContent<any>): Promise<string> {
|
|
41
|
+
let rval: string = null;
|
|
42
|
+
if (data?.content) {
|
|
43
|
+
rval = await WebStreamRatchet.webReadableStreamToString(data.content);
|
|
44
|
+
}
|
|
45
|
+
return rval;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public static async dataAsUint8Array(data: RemoteStatusDataAndContent<any>): Promise<Uint8Array> {
|
|
49
|
+
let rval: Uint8Array = null;
|
|
50
|
+
if (data?.content) {
|
|
51
|
+
rval = await WebStreamRatchet.webReadableStreamToUint8Array(data.content);
|
|
52
|
+
} else {
|
|
53
|
+
Logger.warn('Was unable to read as array since content is missing');
|
|
54
|
+
}
|
|
55
|
+
return rval;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public static async dataAsObject<T>(data: RemoteStatusDataAndContent<any>): Promise<T> {
|
|
59
|
+
let rval: T = null;
|
|
60
|
+
if (data?.content) {
|
|
61
|
+
const txt: string = await this.dataAsString(data);
|
|
62
|
+
rval = JSON.parse(txt);
|
|
63
|
+
}
|
|
64
|
+
return rval;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// Will throw exception if remote was modified since the last update
|
|
68
|
+
public async pushStreamToRemote(src: ReadableStream, inPushOpts?: RemoteFileTrackerPushOptions): Promise<RemoteStatusData<KeyType>> {
|
|
69
|
+
RequireRatchet.notNullOrUndefined(src, 'src');
|
|
70
|
+
const pushOpts: RemoteFileTrackerPushOptions = Object.assign({ force: false, backup: false }, inPushOpts);
|
|
71
|
+
const result: FileTransferResult = await this.opts.provider.sendDataToRemote(src, this.opts.key, pushOpts, this._remoteStatusData);
|
|
72
|
+
if (result.type === FileTransferResultType.Updated) {
|
|
73
|
+
Logger.info('Sent %d bytes to remote, re-reading sync');
|
|
74
|
+
await this.sync();
|
|
75
|
+
}
|
|
76
|
+
return this._remoteStatusData;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
public async pushStringToRemote(src: string, inPushOpts?: RemoteFileTrackerPushOptions): Promise<RemoteStatusData<KeyType>> {
|
|
80
|
+
RequireRatchet.notNullOrUndefined(src, 'src');
|
|
81
|
+
const asString: ReadableStream = WebStreamRatchet.stringToWebReadableStream(src);
|
|
82
|
+
return this.pushStreamToRemote(asString, inPushOpts);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
public async pushUint8ArrayToRemote(src: Uint8Array, inPushOpts?: RemoteFileTrackerPushOptions): Promise<RemoteStatusData<KeyType>> {
|
|
86
|
+
RequireRatchet.notNullOrUndefined(src, 'src');
|
|
87
|
+
const asString: ReadableStream = WebStreamRatchet.uint8ArrayToWebReadableStream(src);
|
|
88
|
+
return this.pushStreamToRemote(asString, inPushOpts);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
public async pushObjectJsonToRemote(src: any, inPushOpts?: RemoteFileTrackerPushOptions): Promise<RemoteStatusData<KeyType>> {
|
|
92
|
+
RequireRatchet.notNullOrUndefined(src, 'src');
|
|
93
|
+
const asString: string = JSON.stringify(src);
|
|
94
|
+
return this.pushStringToRemote(asString, inPushOpts);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
// If skipUpdateLocal is NOT set, the next call will succeed because
|
|
98
|
+
// the data is updated as part of the read
|
|
99
|
+
public async modifiedSinceLastSync(skipUpdateLocal?: boolean): Promise<boolean> {
|
|
100
|
+
const newData: RemoteStatusData<KeyType> = await this.opts.provider.readRemoteStatus(this.opts.key);
|
|
101
|
+
const rval: boolean = !RemoteFileTracker.statusMatch(newData, this._remoteStatusData);
|
|
102
|
+
if (!skipUpdateLocal) {
|
|
103
|
+
this._remoteStatusData = newData;
|
|
104
|
+
}
|
|
105
|
+
return rval;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
public static statusMatch(rsd1: RemoteStatusData<any>, rsd2: RemoteStatusData<any>): boolean {
|
|
109
|
+
return (
|
|
110
|
+
rsd1 &&
|
|
111
|
+
rsd2 &&
|
|
112
|
+
rsd1.remoteHash === rsd2.remoteHash &&
|
|
113
|
+
rsd1.remoteSizeInBytes === rsd2.remoteSizeInBytes &&
|
|
114
|
+
rsd1.remoteLastUpdatedEpochMs === rsd2.remoteLastUpdatedEpochMs
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
// An interface to define an object that provides remote file sync (and keep syncd)
|
|
2
|
+
// capabilities. Mainly in common so I don't have to import AWS into my sqlite package
|
|
3
|
+
// or vice versa
|
|
4
|
+
|
|
5
|
+
import { RemoteStatusData } from './remote-status-data.js';
|
|
6
|
+
import { RemoteStatusDataAndContent } from './remote-status-data-and-content.js';
|
|
7
|
+
import { FileTransferResult } from './file-transfer-result.js';
|
|
8
|
+
import { RemoteFileTrackerPushOptions } from './remote-file-tracker-push-options.js';
|
|
9
|
+
|
|
10
|
+
export interface RemoteFileTrackingProvider<KeyType> {
|
|
11
|
+
readRemoteStatus(key: KeyType): Promise<RemoteStatusData<KeyType>>;
|
|
12
|
+
pullRemoteData(key: KeyType, ifNewerThan?: RemoteStatusData<KeyType>): Promise<RemoteStatusDataAndContent<KeyType>>;
|
|
13
|
+
sendDataToRemote(
|
|
14
|
+
src: ReadableStream,
|
|
15
|
+
key: KeyType,
|
|
16
|
+
opts: RemoteFileTrackerPushOptions,
|
|
17
|
+
checkStatus: RemoteStatusData<KeyType>,
|
|
18
|
+
): Promise<FileTransferResult>;
|
|
19
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { RestfulApiHttpError } from './restful-api-http-error.js';
|
|
2
|
+
import { describe, expect, test } from 'vitest';
|
|
3
|
+
|
|
4
|
+
describe('#restfulApiHttpError', function () {
|
|
5
|
+
test('should check if the error is a given class', async () => {
|
|
6
|
+
const testError: Error = new RestfulApiHttpError('test').withHttpStatusCode(404);
|
|
7
|
+
const nonHttpError: Error = new Error('Not HTTP');
|
|
8
|
+
expect(RestfulApiHttpError.errorIs40x(testError)).toBeTruthy();
|
|
9
|
+
expect(RestfulApiHttpError.errorIs50x(testError)).toBeFalsy();
|
|
10
|
+
expect(RestfulApiHttpError.errorIs40x(nonHttpError)).toBeFalsy();
|
|
11
|
+
expect(RestfulApiHttpError.errorIs50x(testError)).toBeFalsy();
|
|
12
|
+
});
|
|
13
|
+
});
|
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
import { NumberRatchet } from '../lang/number-ratchet.js';
|
|
2
|
+
import { StringRatchet } from '../lang/string-ratchet.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* 2023-07-18 : I moved this class from Epsilon over to common because 1) It has no
|
|
6
|
+
* dependencies so it's a light lift and 2) unicorn showed me it is helpful to have
|
|
7
|
+
* it available on the client side.
|
|
8
|
+
*
|
|
9
|
+
* This class is meant to provide a more robust and standardized error class for
|
|
10
|
+
* throwing things across the http wire in restful apis.
|
|
11
|
+
*/
|
|
12
|
+
export class RestfulApiHttpError<T = void> extends Error {
|
|
13
|
+
private static readonly RATCHET_RESTFUL_API_HTTP_ERROR_FLAG_KEY: string = '__ratchetRestfulApiHttpErrorFlag';
|
|
14
|
+
private _httpStatusCode = 500;
|
|
15
|
+
private _errors: string[];
|
|
16
|
+
private _detailErrorCode: number;
|
|
17
|
+
private _endUserErrors: string[];
|
|
18
|
+
private _details: T;
|
|
19
|
+
private _requestId: string;
|
|
20
|
+
private _wrappedError: Error;
|
|
21
|
+
|
|
22
|
+
constructor(...errors: string[]) {
|
|
23
|
+
super(RestfulApiHttpError.combineErrorStringsWithDefault(errors));
|
|
24
|
+
Object.setPrototypeOf(this, RestfulApiHttpError.prototype);
|
|
25
|
+
this._errors = errors;
|
|
26
|
+
this[RestfulApiHttpError.RATCHET_RESTFUL_API_HTTP_ERROR_FLAG_KEY] = true; // Just used to tell if one has been wrapped
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
public static combineErrorStringsWithDefault(errors: string[], defMessage = 'Internal Server Error'): string {
|
|
30
|
+
return errors && errors.length > 0 ? errors.join(',') : defMessage;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
public setFormattedErrorMessage(format: string, ...input: any[]): void {
|
|
34
|
+
const msg: string = StringRatchet.format(format, ...input);
|
|
35
|
+
this.errors = [msg];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
public withFormattedErrorMessage(format: string, ...input: any[]): RestfulApiHttpError<T> {
|
|
39
|
+
this.setFormattedErrorMessage(format, ...input);
|
|
40
|
+
return this;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
public withHttpStatusCode(httpStatusCode: number): RestfulApiHttpError<T> {
|
|
44
|
+
this.httpStatusCode = httpStatusCode; // Call setter
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public withErrors(errors: string[]): RestfulApiHttpError<T> {
|
|
49
|
+
this.errors = errors; // Call setter
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
public withDetailErrorCode(detailErrorCode: number): RestfulApiHttpError<T> {
|
|
54
|
+
this._detailErrorCode = detailErrorCode; // Call setter
|
|
55
|
+
return this;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
public withEndUserErrors(endUserErrors: string[]): RestfulApiHttpError<T> {
|
|
59
|
+
this._endUserErrors = endUserErrors; // Call setter
|
|
60
|
+
return this;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public withDetails(details: T): RestfulApiHttpError<T> {
|
|
64
|
+
this._details = details; // Call setter
|
|
65
|
+
return this;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public withRequestId(requestId: string): RestfulApiHttpError<T> {
|
|
69
|
+
this._requestId = requestId; // Call setter
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
public withWrappedError(err: Error): RestfulApiHttpError<T> {
|
|
74
|
+
this._wrappedError = err; // Call setter
|
|
75
|
+
return this;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
public isWrappedError(): boolean {
|
|
79
|
+
return !!this._wrappedError;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
public static wrapError<T = void>(err: Error): RestfulApiHttpError<T> {
|
|
83
|
+
let rval: RestfulApiHttpError<T> = null;
|
|
84
|
+
if (RestfulApiHttpError.objectIsRestfulApiHttpError(err)) {
|
|
85
|
+
rval = err as RestfulApiHttpError<T>;
|
|
86
|
+
} else {
|
|
87
|
+
rval = new RestfulApiHttpError<T>(err.message).withWrappedError(err).withHttpStatusCode(500);
|
|
88
|
+
}
|
|
89
|
+
return rval;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
public static objectIsRestfulApiHttpError(obj: any): boolean {
|
|
93
|
+
return obj && obj[RestfulApiHttpError.RATCHET_RESTFUL_API_HTTP_ERROR_FLAG_KEY] === true;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
get httpStatusCode(): number {
|
|
97
|
+
return this._httpStatusCode;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
set httpStatusCode(value: number) {
|
|
101
|
+
this._httpStatusCode = value || 500;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
get errors(): string[] {
|
|
105
|
+
return this._errors;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
set errors(value: string[]) {
|
|
109
|
+
this._errors = value || ['Internal Server Error'];
|
|
110
|
+
this.message = RestfulApiHttpError.combineErrorStringsWithDefault(this._errors);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
get detailErrorCode(): number {
|
|
114
|
+
return this._detailErrorCode;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
set detailErrorCode(value: number) {
|
|
118
|
+
this._detailErrorCode = value;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
get endUserErrors(): string[] {
|
|
122
|
+
return this._endUserErrors;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
set endUserErrors(value: string[]) {
|
|
126
|
+
this._endUserErrors = value;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
get details(): T {
|
|
130
|
+
return this._details;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
set details(value: T) {
|
|
134
|
+
this._details = value;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
get requestId(): string {
|
|
138
|
+
return this._requestId;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
set requestId(value: string) {
|
|
142
|
+
this._requestId = value || 'MISSING';
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
get wrappedError(): Error {
|
|
146
|
+
return this._wrappedError;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
set wrappedError(value: Error) {
|
|
150
|
+
this._wrappedError = value;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
public static errorIsX0x(errIn: Error, xClass: number): boolean {
|
|
154
|
+
let rval = false;
|
|
155
|
+
if (errIn && RestfulApiHttpError.objectIsRestfulApiHttpError(errIn)) {
|
|
156
|
+
const err: RestfulApiHttpError = errIn as RestfulApiHttpError;
|
|
157
|
+
|
|
158
|
+
const val: number = NumberRatchet.safeNumber(err.httpStatusCode);
|
|
159
|
+
const bot: number = xClass * 100;
|
|
160
|
+
const top: number = bot + 99;
|
|
161
|
+
rval = val >= bot && val <= top;
|
|
162
|
+
}
|
|
163
|
+
return rval;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
public static errorIs40x(err: Error): boolean {
|
|
167
|
+
return RestfulApiHttpError.errorIsX0x(err, 4);
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
public static errorIs50x(err: Error): boolean {
|
|
171
|
+
return RestfulApiHttpError.errorIsX0x(err, 5);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* tbd
|
|
3
|
+
* @interface RatchetTemplateRenderer
|
|
4
|
+
*/
|
|
5
|
+
export interface RatchetTemplateRenderer {
|
|
6
|
+
renderTemplate(templateName: string, context: any, layoutName?: string, partialNames?: string[]): Promise<string>;
|
|
7
|
+
renderTemplateDirect(templateValue: string, context: any, layoutName?: string): Promise<string>;
|
|
8
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { GoogleRecaptchaRatchet } from './google-recaptcha-ratchet.js';
|
|
2
|
+
import { describe, expect, test, vi } from 'vitest';
|
|
3
|
+
|
|
4
|
+
const fakeFetch = vi.fn((_input, _init) =>
|
|
5
|
+
Promise.resolve({
|
|
6
|
+
json: () => Promise.resolve({ success: true }),
|
|
7
|
+
} as Response),
|
|
8
|
+
);
|
|
9
|
+
|
|
10
|
+
const fakeFailFetch = vi.fn((_input, _init) => Promise.reject('Failed to read'));
|
|
11
|
+
|
|
12
|
+
describe('#googleRecaptchaService', () => {
|
|
13
|
+
test('should validate a recaptcha token', async () => {
|
|
14
|
+
const res: any = await GoogleRecaptchaRatchet.verifyRecaptchaToken('anykey', 'anytoken', fakeFetch);
|
|
15
|
+
expect(res).toBeTruthy();
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
test('should fail with no token', async () => {
|
|
19
|
+
const res: any = await GoogleRecaptchaRatchet.verifyRecaptchaToken('anykey', null, fakeFetch);
|
|
20
|
+
expect(res).toBeFalsy();
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('should fail if http fails', async () => {
|
|
24
|
+
const res: any = await GoogleRecaptchaRatchet.verifyRecaptchaToken('anykey', 'anytoken', fakeFailFetch);
|
|
25
|
+
expect(res).toBeFalsy();
|
|
26
|
+
});
|
|
27
|
+
});
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Service for functions that communicate to Google Recaptcha service
|
|
3
|
+
*/
|
|
4
|
+
import { Logger } from '../../logger/logger.js';
|
|
5
|
+
import { StringRatchet } from '../../lang/string-ratchet.js';
|
|
6
|
+
import fetch from 'cross-fetch';
|
|
7
|
+
|
|
8
|
+
export class GoogleRecaptchaRatchet {
|
|
9
|
+
private static readonly GOOGLE_VERIFY_URL: string = 'https://www.google.com/recaptcha/api/siteverify?secret=${KEY}&response=${TOKEN}';
|
|
10
|
+
|
|
11
|
+
public static async verifyRecaptchaToken(
|
|
12
|
+
keySecret: string,
|
|
13
|
+
token: string,
|
|
14
|
+
fetchFn: (input: RequestInfo | URL, init?: RequestInit) => Promise<Response> = fetch,
|
|
15
|
+
): Promise<boolean> {
|
|
16
|
+
Logger.debug('Verifying recaptcha token : %s', token);
|
|
17
|
+
let rval: boolean = null;
|
|
18
|
+
|
|
19
|
+
if (!StringRatchet.safeString(token)) {
|
|
20
|
+
Logger.warn('Recaptcha validation error, no token passed : %s', token);
|
|
21
|
+
return rval;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
Logger.info('Validating Recaptcha via Google API : %s', token);
|
|
25
|
+
const url: string = StringRatchet.simpleTemplateFill(GoogleRecaptchaRatchet.GOOGLE_VERIFY_URL, { KEY: keySecret, TOKEN: token }, true);
|
|
26
|
+
try {
|
|
27
|
+
const resp: Response = await fetchFn(url);
|
|
28
|
+
const body: any = await resp.json();
|
|
29
|
+
rval = body && body.success;
|
|
30
|
+
} catch (err) {
|
|
31
|
+
Logger.error('Failed to read from google : %s', err);
|
|
32
|
+
rval = false;
|
|
33
|
+
}
|
|
34
|
+
return rval;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import fetch from 'cross-fetch';
|
|
2
|
+
|
|
3
|
+
import { RequireRatchet } from '../../lang/require-ratchet.js';
|
|
4
|
+
import { Base64Ratchet } from '../../lang/base64-ratchet.js';
|
|
5
|
+
import { StringRatchet } from '../../lang/string-ratchet.js';
|
|
6
|
+
import { Logger } from '../../logger/logger.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* This class is for people who just need to send an occasionaly outbound text message and don't need
|
|
10
|
+
* Twilio's 7mb non-tree-shakable library
|
|
11
|
+
*/
|
|
12
|
+
export class TwilioRatchet {
|
|
13
|
+
// CAW: Switch to api.ashburn to us just us-east-1 ?
|
|
14
|
+
public static readonly TWILLIO_BASE_API_URL: string = 'https://api.twilio.com/2010-04-01';
|
|
15
|
+
|
|
16
|
+
constructor(
|
|
17
|
+
private accountSid: string,
|
|
18
|
+
private authToken: string,
|
|
19
|
+
private outBoundNumber: string,
|
|
20
|
+
) {
|
|
21
|
+
RequireRatchet.notNullOrUndefined(accountSid, 'accountSid');
|
|
22
|
+
RequireRatchet.notNullOrUndefined(authToken, 'authToken');
|
|
23
|
+
RequireRatchet.notNullOrUndefined(outBoundNumber, 'outBoundNumber');
|
|
24
|
+
RequireRatchet.true(TwilioRatchet.isValidE164Number(outBoundNumber), 'outBoundNumber invalid format');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Pass thru for simplification
|
|
28
|
+
public static async sendMessageDirect(
|
|
29
|
+
accountSid: string,
|
|
30
|
+
authToken: string,
|
|
31
|
+
outBoundNumber: string,
|
|
32
|
+
recipientPhoneNumbers: string[],
|
|
33
|
+
message: string,
|
|
34
|
+
): Promise<any[]> {
|
|
35
|
+
const ratchet: TwilioRatchet = new TwilioRatchet(accountSid, authToken, outBoundNumber);
|
|
36
|
+
const rval: any[] = await ratchet.sendMessage(recipientPhoneNumbers, message);
|
|
37
|
+
return rval;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
public static generateTwilioBasicAuth(sid: string, authToken: string): string {
|
|
41
|
+
const authHeader: string = 'Basic ' + Base64Ratchet.generateBase64VersionOfString(sid + ':' + authToken);
|
|
42
|
+
return authHeader;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
public async sendMessage(recipientPhoneNumbers: string[], message: string): Promise<any[]> {
|
|
46
|
+
const rval: any[] = [];
|
|
47
|
+
RequireRatchet.notNullOrUndefined(recipientPhoneNumbers, 'recipientPhoneNumbers');
|
|
48
|
+
RequireRatchet.notNullOrUndefined(StringRatchet.trimToNull(message), 'message');
|
|
49
|
+
RequireRatchet.true(recipientPhoneNumbers.length > 0, 'recipientPhoneNumbers non-empty');
|
|
50
|
+
|
|
51
|
+
if (!!recipientPhoneNumbers && recipientPhoneNumbers.length > 0 && !!StringRatchet.trimToNull(message)) {
|
|
52
|
+
Logger.info('Sending %s to %j', message, recipientPhoneNumbers);
|
|
53
|
+
|
|
54
|
+
for (const phoneNumber of recipientPhoneNumbers) {
|
|
55
|
+
Logger.info('To: %s', phoneNumber);
|
|
56
|
+
|
|
57
|
+
if (!TwilioRatchet.isValidE164Number(phoneNumber)) {
|
|
58
|
+
throw new Error('number must be E164 format!');
|
|
59
|
+
}
|
|
60
|
+
const body: string =
|
|
61
|
+
'Body=' +
|
|
62
|
+
encodeURIComponent(message) +
|
|
63
|
+
'&From=' +
|
|
64
|
+
encodeURIComponent(this.outBoundNumber) +
|
|
65
|
+
'&To=' +
|
|
66
|
+
encodeURIComponent(phoneNumber);
|
|
67
|
+
const post: any = {
|
|
68
|
+
method: 'post',
|
|
69
|
+
headers: {
|
|
70
|
+
authorization: TwilioRatchet.generateTwilioBasicAuth(this.accountSid, this.authToken),
|
|
71
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
72
|
+
},
|
|
73
|
+
body: body,
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const res: Response = await fetch(TwilioRatchet.TWILLIO_BASE_API_URL + '/Accounts/' + this.accountSid + '/Messages.json', post);
|
|
77
|
+
const parsedResponse: any = await res.json();
|
|
78
|
+
Logger.debug('TwilioRatchet: For %s got %j', phoneNumber, parsedResponse);
|
|
79
|
+
rval.push(parsedResponse);
|
|
80
|
+
}
|
|
81
|
+
} else {
|
|
82
|
+
Logger.warn('Not sending empty message / empty recipients');
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
return rval;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Validate E164 format
|
|
89
|
+
public static isValidE164Number(num: string): boolean {
|
|
90
|
+
return /^\+?[1-9]\d{1,14}$/.test(num);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import fetch from 'cross-fetch';
|
|
2
|
+
|
|
3
|
+
import { RequireRatchet } from '../../lang/require-ratchet.js';
|
|
4
|
+
import { Logger } from '../../logger/logger.js';
|
|
5
|
+
import { TwilioRatchet } from './twilio-ratchet.js';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* This is for JUST hitting the Twilio verify service (useful for 2FA/One time passwords) without
|
|
9
|
+
* needing a phone number
|
|
10
|
+
*/
|
|
11
|
+
export class TwilioVerifyRatchet {
|
|
12
|
+
public static readonly TWILLIO_BASE_VERIFY_URL: string = 'https://verify.twilio.com/v2/Services/';
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private accountSid: string,
|
|
16
|
+
private authToken: string,
|
|
17
|
+
private serviceSid: string,
|
|
18
|
+
) {
|
|
19
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(accountSid, 'accountSid');
|
|
20
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(authToken, 'authToken');
|
|
21
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(serviceSid, 'serviceSid');
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
public async sendVerificationTokenUsingTwilioVerify(recipientPhoneNumber: string): Promise<string> {
|
|
25
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(recipientPhoneNumber, 'recipientPhoneNumber');
|
|
26
|
+
RequireRatchet.true(TwilioRatchet.isValidE164Number(recipientPhoneNumber), 'recipientPhoneNumber must be E164');
|
|
27
|
+
const phone: string = recipientPhoneNumber.startsWith('+1') ? recipientPhoneNumber : '+1' + recipientPhoneNumber;
|
|
28
|
+
const body: string = 'Channel=sms&To=' + encodeURIComponent(phone);
|
|
29
|
+
const post: any = {
|
|
30
|
+
method: 'post',
|
|
31
|
+
headers: {
|
|
32
|
+
authorization: TwilioRatchet.generateTwilioBasicAuth(this.accountSid, this.authToken),
|
|
33
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
34
|
+
},
|
|
35
|
+
body: body,
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const url: string = TwilioVerifyRatchet.TWILLIO_BASE_VERIFY_URL + this.serviceSid + '/Verifications';
|
|
39
|
+
const res: Response = await fetch(url, post);
|
|
40
|
+
const respBody: string = await res.text();
|
|
41
|
+
return respBody;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
public async simpleCheckVerificationTokenUsingTwilioVerify(recipientPhoneNumber: string, code: string): Promise<boolean> {
|
|
45
|
+
const val: TwilioVerifyCheckResponse = await this.checkVerificationTokenUsingTwilioVerify(recipientPhoneNumber, code);
|
|
46
|
+
return val && val.status === 'approved';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
public async checkVerificationTokenUsingTwilioVerify(recipientPhoneNumber: string, code: string): Promise<TwilioVerifyCheckResponse> {
|
|
50
|
+
RequireRatchet.notNullUndefinedOrOnlyWhitespaceString(recipientPhoneNumber, 'recipientPhoneNumber');
|
|
51
|
+
RequireRatchet.true(TwilioRatchet.isValidE164Number(recipientPhoneNumber), 'recipientPhoneNumber must be E164');
|
|
52
|
+
const phone: string = recipientPhoneNumber.startsWith('+1') ? recipientPhoneNumber : '+1' + recipientPhoneNumber;
|
|
53
|
+
const body: string = 'To=' + encodeURIComponent(phone) + '&Code=' + encodeURIComponent(code);
|
|
54
|
+
const post: any = {
|
|
55
|
+
method: 'post',
|
|
56
|
+
headers: {
|
|
57
|
+
authorization: TwilioRatchet.generateTwilioBasicAuth(this.accountSid, this.authToken),
|
|
58
|
+
'Content-Type': 'application/x-www-form-urlencoded',
|
|
59
|
+
},
|
|
60
|
+
body: body,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const url: string = TwilioVerifyRatchet.TWILLIO_BASE_VERIFY_URL + this.serviceSid + '/VerificationCheck';
|
|
64
|
+
Logger.info('Using %s / %j', url, post);
|
|
65
|
+
const res: Response = await fetch(url, post);
|
|
66
|
+
const parsedResponse: TwilioVerifyCheckResponse = await res.json();
|
|
67
|
+
return parsedResponse;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export interface TwilioVerifyCheckResponse {
|
|
72
|
+
status: string;
|
|
73
|
+
payee: string;
|
|
74
|
+
date_updated: string;
|
|
75
|
+
account_sid: string;
|
|
76
|
+
to: string;
|
|
77
|
+
amount: string;
|
|
78
|
+
valid: boolean;
|
|
79
|
+
sid: string;
|
|
80
|
+
date_created: string;
|
|
81
|
+
service_sid: string;
|
|
82
|
+
channel: string;
|
|
83
|
+
}
|