@tuturuuu/utils 0.8.0 → 0.9.0
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/CHANGELOG.md +14 -0
- package/biome.json +1 -1
- package/package.json +17 -15
- package/src/__tests__/api-proxy-guard.test.ts +513 -2
- package/src/__tests__/app-url.test.ts +2 -0
- package/src/abuse-protection/__tests__/backend-rate-limit.test.ts +10 -45
- package/src/abuse-protection/__tests__/edge-trust.test.ts +58 -0
- package/src/abuse-protection/__tests__/index.test.ts +136 -0
- package/src/abuse-protection/backend-rate-limit.ts +5 -4
- package/src/abuse-protection/edge-trust.ts +74 -6
- package/src/abuse-protection/index.ts +73 -40
- package/src/api-proxy-guard.ts +131 -33
- package/src/currencies.test.ts +33 -0
- package/src/currencies.ts +28 -5
- package/src/effect.test.ts +247 -0
- package/src/effect.ts +259 -0
- package/src/internal-domains.ts +24 -0
- package/src/platform-release.test.ts +1 -1
- package/src/platform-release.ts +1 -1
- package/src/portless.ts +2 -0
- package/src/task-helper/task-operations.ts +3 -0
package/src/effect.ts
ADDED
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Cause,
|
|
3
|
+
Context,
|
|
4
|
+
Data,
|
|
5
|
+
Duration,
|
|
6
|
+
Effect,
|
|
7
|
+
Either,
|
|
8
|
+
Exit,
|
|
9
|
+
Layer,
|
|
10
|
+
Option,
|
|
11
|
+
Schedule,
|
|
12
|
+
} from 'effect';
|
|
13
|
+
|
|
14
|
+
export {
|
|
15
|
+
Cause,
|
|
16
|
+
Context,
|
|
17
|
+
Data,
|
|
18
|
+
Duration,
|
|
19
|
+
Effect,
|
|
20
|
+
Either,
|
|
21
|
+
Layer,
|
|
22
|
+
Option,
|
|
23
|
+
Schedule,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export type TuturuuuEffectErrorContext = Record<string, unknown>;
|
|
27
|
+
|
|
28
|
+
export interface TuturuuuEffectErrorOptions {
|
|
29
|
+
readonly code: string;
|
|
30
|
+
readonly message: string;
|
|
31
|
+
readonly status?: number;
|
|
32
|
+
readonly cause?: unknown;
|
|
33
|
+
readonly context?: TuturuuuEffectErrorContext;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class TuturuuuEffectError extends Data.TaggedError(
|
|
37
|
+
'TuturuuuEffectError'
|
|
38
|
+
)<TuturuuuEffectErrorOptions> {}
|
|
39
|
+
|
|
40
|
+
export interface SerializedTuturuuuEffectError {
|
|
41
|
+
readonly _tag: 'TuturuuuEffectError';
|
|
42
|
+
readonly code: string;
|
|
43
|
+
readonly message: string;
|
|
44
|
+
readonly status?: number;
|
|
45
|
+
readonly context?: TuturuuuEffectErrorContext;
|
|
46
|
+
readonly cause?: {
|
|
47
|
+
readonly name?: string;
|
|
48
|
+
readonly message: string;
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export type TuturuuuEffectResult<T> =
|
|
53
|
+
| {
|
|
54
|
+
readonly ok: true;
|
|
55
|
+
readonly data: T;
|
|
56
|
+
}
|
|
57
|
+
| {
|
|
58
|
+
readonly ok: false;
|
|
59
|
+
readonly error: SerializedTuturuuuEffectError;
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
export type DataErrorResult<TData, TError = unknown> = {
|
|
63
|
+
readonly data: TData;
|
|
64
|
+
readonly error: TError | null;
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
export interface TuturuuuEffectRetryOptions<E = unknown> {
|
|
68
|
+
readonly times?: number;
|
|
69
|
+
readonly delay?: Duration.DurationInput;
|
|
70
|
+
readonly while?: (error: E) => boolean;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface TuturuuuEffectTimeoutOptions
|
|
74
|
+
extends TuturuuuEffectErrorOptions {
|
|
75
|
+
readonly duration: Duration.DurationInput;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export interface TuturuuuEffectConcurrencyOptions {
|
|
79
|
+
readonly concurrency?: number | 'unbounded';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export const DEFAULT_TUTURUUU_EFFECT_CONCURRENCY = 4;
|
|
83
|
+
|
|
84
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
85
|
+
return typeof value === 'object' && value !== null;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function readStringProperty(
|
|
89
|
+
source: Record<string, unknown> | undefined,
|
|
90
|
+
key: string
|
|
91
|
+
) {
|
|
92
|
+
const value = source?.[key];
|
|
93
|
+
return typeof value === 'string' && value.trim() ? value : undefined;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function readNumberProperty(
|
|
97
|
+
source: Record<string, unknown> | undefined,
|
|
98
|
+
key: string
|
|
99
|
+
) {
|
|
100
|
+
const value = source?.[key];
|
|
101
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
102
|
+
? value
|
|
103
|
+
: undefined;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildContext(
|
|
107
|
+
source: Record<string, unknown> | undefined,
|
|
108
|
+
fallbackContext: TuturuuuEffectErrorContext | undefined
|
|
109
|
+
) {
|
|
110
|
+
const context: TuturuuuEffectErrorContext = { ...(fallbackContext ?? {}) };
|
|
111
|
+
const details = readStringProperty(source, 'details');
|
|
112
|
+
const hint = readStringProperty(source, 'hint');
|
|
113
|
+
|
|
114
|
+
if (details) context.details = details;
|
|
115
|
+
if (hint) context.hint = hint;
|
|
116
|
+
|
|
117
|
+
return Object.keys(context).length > 0 ? context : undefined;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function toTuturuuuEffectError(
|
|
121
|
+
error: unknown,
|
|
122
|
+
fallback: TuturuuuEffectErrorOptions
|
|
123
|
+
) {
|
|
124
|
+
if (error instanceof TuturuuuEffectError) {
|
|
125
|
+
return error;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const source = isRecord(error) ? error : undefined;
|
|
129
|
+
const message =
|
|
130
|
+
error instanceof Error && error.message
|
|
131
|
+
? error.message
|
|
132
|
+
: readStringProperty(source, 'message') || fallback.message;
|
|
133
|
+
|
|
134
|
+
return new TuturuuuEffectError({
|
|
135
|
+
code: readStringProperty(source, 'code') || fallback.code,
|
|
136
|
+
message,
|
|
137
|
+
status: readNumberProperty(source, 'status') ?? fallback.status,
|
|
138
|
+
cause: error,
|
|
139
|
+
context: buildContext(source, fallback.context),
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function serializeTuturuuuEffectError(
|
|
144
|
+
error: TuturuuuEffectError
|
|
145
|
+
): SerializedTuturuuuEffectError {
|
|
146
|
+
return {
|
|
147
|
+
_tag: error._tag,
|
|
148
|
+
code: error.code,
|
|
149
|
+
message: error.message,
|
|
150
|
+
...(error.status !== undefined ? { status: error.status } : {}),
|
|
151
|
+
...(error.context ? { context: error.context } : {}),
|
|
152
|
+
...(error.cause instanceof Error
|
|
153
|
+
? {
|
|
154
|
+
cause: {
|
|
155
|
+
name: error.cause.name,
|
|
156
|
+
message: error.cause.message,
|
|
157
|
+
},
|
|
158
|
+
}
|
|
159
|
+
: {}),
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
export function fromPromise<T>(
|
|
164
|
+
evaluate: (signal: AbortSignal) => PromiseLike<T>,
|
|
165
|
+
fallback: TuturuuuEffectErrorOptions
|
|
166
|
+
): Effect.Effect<T, TuturuuuEffectError> {
|
|
167
|
+
return Effect.tryPromise({
|
|
168
|
+
try: evaluate,
|
|
169
|
+
catch: (error) => toTuturuuuEffectError(error, fallback),
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export function fromDataError<TData, TError = unknown>(
|
|
174
|
+
operation: (
|
|
175
|
+
signal: AbortSignal
|
|
176
|
+
) => PromiseLike<DataErrorResult<TData, TError>>,
|
|
177
|
+
fallback: TuturuuuEffectErrorOptions
|
|
178
|
+
): Effect.Effect<TData, TuturuuuEffectError> {
|
|
179
|
+
return Effect.flatMap(fromPromise(operation, fallback), (result) =>
|
|
180
|
+
result.error
|
|
181
|
+
? Effect.fail(toTuturuuuEffectError(result.error, fallback))
|
|
182
|
+
: Effect.succeed(result.data)
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
export function withTuturuuuRetry<A, E, R>(
|
|
187
|
+
program: Effect.Effect<A, E, R>,
|
|
188
|
+
options: TuturuuuEffectRetryOptions<E> = {}
|
|
189
|
+
): Effect.Effect<A, E, R> {
|
|
190
|
+
const times = Math.max(0, options.times ?? 2);
|
|
191
|
+
|
|
192
|
+
if (times === 0) {
|
|
193
|
+
return program;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
return program.pipe(
|
|
197
|
+
Effect.retry({
|
|
198
|
+
...(options.delay ? { schedule: Schedule.spaced(options.delay) } : {}),
|
|
199
|
+
times,
|
|
200
|
+
...(options.while ? { while: options.while } : {}),
|
|
201
|
+
})
|
|
202
|
+
);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export function withTuturuuuTimeout<A, E, R>(
|
|
206
|
+
program: Effect.Effect<A, E, R>,
|
|
207
|
+
options: TuturuuuEffectTimeoutOptions
|
|
208
|
+
): Effect.Effect<A, E | TuturuuuEffectError, R> {
|
|
209
|
+
return program.pipe(
|
|
210
|
+
Effect.timeoutFail({
|
|
211
|
+
duration: options.duration,
|
|
212
|
+
onTimeout: () =>
|
|
213
|
+
new TuturuuuEffectError({
|
|
214
|
+
code: options.code,
|
|
215
|
+
message: options.message,
|
|
216
|
+
status: options.status,
|
|
217
|
+
cause: options.cause,
|
|
218
|
+
context: options.context,
|
|
219
|
+
}),
|
|
220
|
+
})
|
|
221
|
+
);
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
export function forEachConcurrently<A, B, E, R>(
|
|
225
|
+
items: Iterable<A>,
|
|
226
|
+
evaluate: (item: A, index: number) => Effect.Effect<B, E, R>,
|
|
227
|
+
options: TuturuuuEffectConcurrencyOptions = {}
|
|
228
|
+
): Effect.Effect<readonly B[], E, R> {
|
|
229
|
+
return Effect.forEach(items, evaluate, {
|
|
230
|
+
concurrency: options.concurrency ?? DEFAULT_TUTURUUU_EFFECT_CONCURRENCY,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
export async function runEffectAsResult<T>(
|
|
235
|
+
program: Effect.Effect<T, TuturuuuEffectError, never>
|
|
236
|
+
): Promise<TuturuuuEffectResult<T>> {
|
|
237
|
+
const exit = await Effect.runPromiseExit(program);
|
|
238
|
+
|
|
239
|
+
if (Exit.isSuccess(exit)) {
|
|
240
|
+
return {
|
|
241
|
+
ok: true,
|
|
242
|
+
data: exit.value,
|
|
243
|
+
};
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
const failure = Cause.failureOption(exit.cause);
|
|
247
|
+
const error = Option.isSome(failure)
|
|
248
|
+
? failure.value
|
|
249
|
+
: new TuturuuuEffectError({
|
|
250
|
+
code: 'EFFECT_DEFECT',
|
|
251
|
+
message: Cause.pretty(exit.cause),
|
|
252
|
+
cause: exit.cause,
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
return {
|
|
256
|
+
ok: false,
|
|
257
|
+
error: serializeTuturuuuEffectError(error),
|
|
258
|
+
};
|
|
259
|
+
}
|
package/src/internal-domains.ts
CHANGED
|
@@ -60,6 +60,10 @@ export const PRODUCTION_INTERNAL_APP_DOMAINS = [
|
|
|
60
60
|
name: 'finance',
|
|
61
61
|
url: 'https://finance.tuturuuu.com',
|
|
62
62
|
},
|
|
63
|
+
{
|
|
64
|
+
name: 'infra',
|
|
65
|
+
url: 'https://infra.tuturuuu.com',
|
|
66
|
+
},
|
|
63
67
|
{
|
|
64
68
|
name: 'inventory',
|
|
65
69
|
url: 'https://inventory.tuturuuu.com',
|
|
@@ -68,6 +72,10 @@ export const PRODUCTION_INTERNAL_APP_DOMAINS = [
|
|
|
68
72
|
name: 'storefront',
|
|
69
73
|
url: 'https://storefront.tuturuuu.com',
|
|
70
74
|
},
|
|
75
|
+
{
|
|
76
|
+
name: 'tanstack-web',
|
|
77
|
+
url: 'https://tanstack.tuturuuu.com',
|
|
78
|
+
},
|
|
71
79
|
{
|
|
72
80
|
name: 'track',
|
|
73
81
|
url: 'https://track.tuturuuu.com',
|
|
@@ -143,6 +151,10 @@ export const PORTLESS_INTERNAL_APP_DOMAINS = [
|
|
|
143
151
|
name: 'finance',
|
|
144
152
|
url: getTuturuuuPortlessAppOrigin('finance'),
|
|
145
153
|
},
|
|
154
|
+
{
|
|
155
|
+
name: 'infra',
|
|
156
|
+
url: getTuturuuuPortlessAppOrigin('infra'),
|
|
157
|
+
},
|
|
146
158
|
{
|
|
147
159
|
name: 'inventory',
|
|
148
160
|
url: getTuturuuuPortlessAppOrigin('inventory'),
|
|
@@ -151,6 +163,10 @@ export const PORTLESS_INTERNAL_APP_DOMAINS = [
|
|
|
151
163
|
name: 'storefront',
|
|
152
164
|
url: getTuturuuuPortlessAppOrigin('storefront'),
|
|
153
165
|
},
|
|
166
|
+
{
|
|
167
|
+
name: 'tanstack-web',
|
|
168
|
+
url: getTuturuuuPortlessAppOrigin('tanstack-web'),
|
|
169
|
+
},
|
|
154
170
|
{
|
|
155
171
|
name: 'track',
|
|
156
172
|
url: getTuturuuuPortlessAppOrigin('track'),
|
|
@@ -226,6 +242,10 @@ export const LOCALHOST_INTERNAL_APP_DOMAINS = [
|
|
|
226
242
|
name: 'finance',
|
|
227
243
|
url: 'http://localhost:7808',
|
|
228
244
|
},
|
|
245
|
+
{
|
|
246
|
+
name: 'infra',
|
|
247
|
+
url: 'http://localhost:7823',
|
|
248
|
+
},
|
|
229
249
|
{
|
|
230
250
|
name: 'inventory',
|
|
231
251
|
url: 'http://localhost:7815',
|
|
@@ -234,6 +254,10 @@ export const LOCALHOST_INTERNAL_APP_DOMAINS = [
|
|
|
234
254
|
name: 'storefront',
|
|
235
255
|
url: 'http://localhost:7822',
|
|
236
256
|
},
|
|
257
|
+
{
|
|
258
|
+
name: 'tanstack-web',
|
|
259
|
+
url: 'http://localhost:7824',
|
|
260
|
+
},
|
|
237
261
|
{
|
|
238
262
|
name: 'track',
|
|
239
263
|
url: 'http://localhost:7810',
|
|
@@ -7,7 +7,7 @@ import {
|
|
|
7
7
|
|
|
8
8
|
describe('platform release metadata', () => {
|
|
9
9
|
it('uses the centralized shared browser app version', () => {
|
|
10
|
-
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.
|
|
10
|
+
expect(TUTURUUU_PLATFORM_VERSION).toBe('0.13.0'); // x-release-please-version
|
|
11
11
|
});
|
|
12
12
|
|
|
13
13
|
it('normalizes generated metadata and derives a short hash', () => {
|
package/src/platform-release.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PLATFORM_BUILD_METADATA } from './generated/platform-build-metadata';
|
|
2
2
|
|
|
3
|
-
export const TUTURUUU_PLATFORM_VERSION = '0.
|
|
3
|
+
export const TUTURUUU_PLATFORM_VERSION = '0.13.0'; // x-release-please-version
|
|
4
4
|
|
|
5
5
|
export type PlatformBuildMetadataInput = {
|
|
6
6
|
builtAt?: string | null;
|
package/src/portless.ts
CHANGED
|
@@ -15,6 +15,7 @@ export const TUTURUUU_PORTLESS_APP_ORIGINS = {
|
|
|
15
15
|
finance: `https://finance.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
16
16
|
hive: `https://hive.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
17
17
|
'hive-realtime': `https://realtime.hive.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
18
|
+
infra: `https://infra.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
18
19
|
inventory: `https://inventory.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
19
20
|
learn: `https://learn.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
20
21
|
mail: `https://mail.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
@@ -27,6 +28,7 @@ export const TUTURUUU_PORTLESS_APP_ORIGINS = {
|
|
|
27
28
|
rewise: `https://rewise.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
28
29
|
shortener: `https://shortener.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
29
30
|
storefront: `https://storefront.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
31
|
+
'tanstack-web': `https://tanstack.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
30
32
|
tasks: `https://tasks.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
31
33
|
teach: `https://teach.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
32
34
|
track: `https://track.${TUTURUUU_PORTLESS_ROOT_HOST}`,
|
|
@@ -165,6 +165,9 @@ export async function invalidateTaskCaches(
|
|
|
165
165
|
promises.push(
|
|
166
166
|
queryClient.invalidateQueries({ queryKey: ['task_lists', boardId] })
|
|
167
167
|
);
|
|
168
|
+
promises.push(
|
|
169
|
+
queryClient.invalidateQueries({ queryKey: ['kanban-deadline-tasks'] })
|
|
170
|
+
);
|
|
168
171
|
}
|
|
169
172
|
|
|
170
173
|
promises.push(
|