@cripty2001/utils 0.0.244 → 0.0.246

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.
@@ -3,49 +3,148 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.AppserverDispatcher = exports.AppserverHandledError = exports.AppserverAuthError = exports.FormatRegistry = exports.Value = exports.Type = void 0;
7
- const msgpack_1 = require("@msgpack/msgpack");
8
- const value_1 = require("@sinclair/typebox/value");
9
- const express_1 = __importDefault(require("express"));
10
- const promises_1 = require("fs/promises");
6
+ exports.AppserverUserHandledError = exports.AppserverAuthHandledError = exports.Appserver = exports.Type = void 0;
7
+ const fastify_1 = __importDefault(require("fastify"));
8
+ const type_provider_typebox_1 = require("@fastify/type-provider-typebox");
9
+ Object.defineProperty(exports, "Type", { enumerable: true, get: function () { return type_provider_typebox_1.Type; } });
11
10
  const path_1 = __importDefault(require("path"));
12
- // Helpful for avoiding sinclair version mismatch between this and the actual user of the package
13
- var typebox_1 = require("@sinclair/typebox");
14
- Object.defineProperty(exports, "Type", { enumerable: true, get: function () { return typebox_1.Type; } });
15
- var value_2 = require("@sinclair/typebox/value");
16
- Object.defineProperty(exports, "Value", { enumerable: true, get: function () { return value_2.Value; } });
17
- var typebox_2 = require("@sinclair/typebox");
18
- Object.defineProperty(exports, "FormatRegistry", { enumerable: true, get: function () { return typebox_2.FormatRegistry; } });
19
- (0, msgpack_1.encode)({}); // Fixes issue with msgpack not being included in build
20
- class AppserverError extends Error {
21
- code;
22
- payload;
23
- status;
24
- constructor(code, message, payload = {}, status = 500) {
25
- super(message);
26
- this.code = code;
27
- this.payload = payload;
28
- this.status = status;
11
+ const promises_1 = require("fs/promises");
12
+ const jose_1 = require("jose");
13
+ class Appserver {
14
+ fastify = (0, fastify_1.default)({ logger: true })
15
+ .withTypeProvider();
16
+ jwks;
17
+ issuer;
18
+ audience;
19
+ static async resolveAuth(baseUrl) {
20
+ const res = await fetch(`${baseUrl}/.well-known/openid-configuration`);
21
+ if (!res.ok)
22
+ throw new Error(`OIDC discovery failed: ${res.status}`);
23
+ const config = await res.json();
24
+ if (!config?.jwks_uri)
25
+ throw new Error('OIDC discovery failed: jwks_uri not found');
26
+ if (!config.issuer)
27
+ throw new Error('OIDC discovery failed: issuer not found');
28
+ const jwksUrl = config.jwks_uri;
29
+ const issuer = config.issuer;
30
+ const jwks = (0, jose_1.createRemoteJWKSet)(new URL(jwksUrl));
31
+ return {
32
+ jwks,
33
+ issuer
34
+ };
29
35
  }
30
- }
31
- class AppserverAuthError extends AppserverError {
32
- constructor(message = "You have no right to access this page", code = "PERMISSION_DENIED") {
33
- super(code, message, {}, 403);
36
+ rpcBase;
37
+ constructor(rpcBase, jwks, issuer, audience) {
38
+ this.rpcBase = rpcBase;
39
+ this.jwks = jwks;
40
+ this.issuer = issuer;
41
+ this.audience = audience;
34
42
  }
35
- }
36
- exports.AppserverAuthError = AppserverAuthError;
37
- class AppserverHandledError extends AppserverError {
38
- constructor(code, message, payload = {}) {
39
- super(code, message, payload);
43
+ static async create(audience, issuerUrl, rpcBase = '/exec') {
44
+ const { jwks, issuer } = await this.resolveAuth(issuerUrl);
45
+ const appserver = new Appserver(rpcBase, jwks, issuer, audience);
46
+ appserver.fastify.setErrorHandler(appserver.handleError
47
+ .bind(appserver));
48
+ appserver.fastify.addContentTypeParser('application/octet-stream', { parseAs: 'buffer', bodyLimit: 64 * 1024 * 1024 }, (_req, body, done) => {
49
+ done(null, body);
50
+ });
51
+ appserver.fastify.decorateRequest('user', null);
52
+ return appserver;
40
53
  }
41
- }
42
- exports.AppserverHandledError = AppserverHandledError;
43
- class AppserverDispatcher {
44
- registry = new Map();
45
- register(action, mod) {
46
- if (this.registry.has(action))
47
- throw new Error(`Action already registered: "${action}"`);
48
- this.registry.set(action, mod);
54
+ async listen(port) {
55
+ await this.fastify.listen({
56
+ port,
57
+ host: '0.0.0.0'
58
+ });
59
+ }
60
+ registerRpc(route, schema, scope, handler) {
61
+ const routeSchema = {
62
+ body: schema.input,
63
+ response: {
64
+ 200: schema.output,
65
+ },
66
+ };
67
+ this.fastify.post(`${this.rpcBase}/${route.replace(/^\/+/, '')}`, {
68
+ preHandler: [
69
+ this.enforceAccess(scope)
70
+ ],
71
+ schema: routeSchema,
72
+ }, async (request) => {
73
+ const user = request.user;
74
+ return handler(request.body, user);
75
+ });
76
+ }
77
+ enforceAccess(scope) {
78
+ return async (request, _reply) => {
79
+ const token = request.headers.authorization;
80
+ if (!token || !token.startsWith('Bearer '))
81
+ throw new AppserverAuthHandledError('HEADER_MISSING', 'Missing or invalid authorization header', true);
82
+ const payload = await (async () => {
83
+ try {
84
+ const { payload } = await (0, jose_1.jwtVerify)(token.slice(7), this.jwks, {
85
+ issuer: this.issuer,
86
+ audience: this.audience,
87
+ requiredClaims: ['sub'],
88
+ });
89
+ return payload;
90
+ }
91
+ catch (err) {
92
+ if (err instanceof jose_1.errors.JWTExpired)
93
+ throw new AppserverAuthHandledError('TOKEN_EXPIRED', 'Access token has expired.', true);
94
+ if (err instanceof jose_1.errors.JWTInvalid
95
+ || err instanceof jose_1.errors.JWTClaimValidationFailed
96
+ || err instanceof jose_1.errors.JWSSignatureVerificationFailed
97
+ || err instanceof jose_1.errors.JWSInvalid
98
+ || err instanceof jose_1.errors.JWKSNoMatchingKey
99
+ || err instanceof jose_1.errors.JOSEAlgNotAllowed)
100
+ throw new AppserverAuthHandledError('TOKEN_INVALID', 'Access token is invalid', true);
101
+ if (err instanceof jose_1.errors.JWKSTimeout || err instanceof jose_1.errors.JWKSInvalid)
102
+ throw new Error('Failed to verify the access token against the identity provider JWKS.', { cause: err });
103
+ if (err instanceof Error)
104
+ throw err;
105
+ throw new Error('Unexpected failure while verifying the access token.', {
106
+ cause: err
107
+ });
108
+ }
109
+ })();
110
+ const scopes = payload.scope?.split(' ') ?? [];
111
+ if (!scopes.includes(`res:${scope}`))
112
+ throw new AppserverAuthHandledError('MISSING_SCOPE', 'Access token scope is not valid.', false);
113
+ if (!payload.sub)
114
+ throw new Error('JOSE assert failed, sub is missing despite being required');
115
+ request.user = {
116
+ id: payload.sub
117
+ };
118
+ };
119
+ }
120
+ handleError(error, request, reply) {
121
+ if (error.validation)
122
+ return reply
123
+ .code(422)
124
+ .send({
125
+ errors: error.validation,
126
+ });
127
+ if (error instanceof AppserverHandledError)
128
+ return reply
129
+ .status(error.status)
130
+ .send({
131
+ code: error.code,
132
+ message: error.message,
133
+ details: error.details,
134
+ });
135
+ request.log.error({
136
+ error: {
137
+ name: error.name,
138
+ message: error.message,
139
+ stack: error.stack,
140
+ }
141
+ }, 'unhandled error');
142
+ reply
143
+ .status(500)
144
+ .send({
145
+ code: 'INTERNAL_ERROR',
146
+ message: 'Internal server error',
147
+ });
49
148
  }
50
149
  async autoload(base) {
51
150
  const absBase = path_1.default.resolve(base);
@@ -53,7 +152,8 @@ class AppserverDispatcher {
53
152
  recursive: true,
54
153
  withFileTypes: true
55
154
  });
56
- const jsFiles = files.filter(e => e.isFile() && e.name.endsWith('.js'));
155
+ const jsFiles = files
156
+ .filter(e => e.isFile() && e.name.endsWith('.js'));
57
157
  if (jsFiles.length === 0) {
58
158
  console.warn(`[appserver] autoload found no .js files in "${absBase}".`, `If you are using TypeScript, make sure the api directory is included in your tsconfig`, `and that you are pointing autoload at the compiled output directory, not the source.`);
59
159
  return;
@@ -69,70 +169,33 @@ class AppserverDispatcher {
69
169
  if (!ACTION_REGEX.test(action))
70
170
  throw new Error(`Invalid module path "${action}" — all segments must be lowercase alphanumeric`);
71
171
  const mod = await import(fullPath);
72
- if (!mod.default || typeof mod.default.handler !== 'function' || !mod.default.schema)
73
- throw new Error(`Module "${fullPath}" must have a default export satisfying ApiModule`);
74
- this.register(action, mod.default);
172
+ if (!mod.default)
173
+ throw new Error(`Module "${fullPath}" must have a default export containing a registering function (register(app: Appserver) => void)`);
174
+ mod.default(this, action);
75
175
  }
76
176
  }
77
- bind(app, path) {
78
- app.post(path, express_1.default.raw({
79
- type: 'application/vnd.msgpack',
80
- limit: '2gb'
81
- }), async (req, res) => {
82
- await this.dispatch(req, res);
83
- });
177
+ }
178
+ exports.Appserver = Appserver;
179
+ class AppserverHandledError extends Error {
180
+ status;
181
+ code;
182
+ details;
183
+ constructor(code, message, status, details) {
184
+ super(message);
185
+ this.status = status;
186
+ this.code = code;
187
+ this.details = details;
84
188
  }
85
- async dispatch(req, res) {
86
- const respond = (status, data) => {
87
- res
88
- .status(status)
89
- .send((0, msgpack_1.encode)(data));
90
- };
91
- try {
92
- if (req.headers['content-type'] !== 'application/vnd.msgpack')
93
- throw new AppserverError('REQUEST_INVALID_TYPE_HEADER', 'Content-Type must be a messagepack (application/vnd.msgpack)', {}, 400);
94
- const parsed = (() => {
95
- try {
96
- return (0, msgpack_1.decode)(req.body);
97
- }
98
- catch (e) {
99
- throw new AppserverError('REQUEST_INVALID_BODY', 'Request body is not valid msgpack', {}, 400);
100
- }
101
- })();
102
- const { action, payload } = parsed;
103
- if (typeof action !== 'string')
104
- throw new AppserverError("REQUEST_INVALID_ACTION", "Missing action field", {}, 400);
105
- const mod = this.registry.get(action);
106
- if (mod === undefined)
107
- throw new AppserverError("ACTION_NOT_FOUND", `Action not found: ${action}`, {}, 404);
108
- const token = req.headers['authorization']?.startsWith('Bearer ') ?
109
- req.headers['authorization'].slice(7) :
110
- undefined;
111
- if (!value_1.Value.Check(mod.schema, payload))
112
- return respond(422, {
113
- errors: [...value_1.Value.Errors(mod.schema, payload)]
114
- .map(e => ({
115
- path: e.path,
116
- message: e.message,
117
- value: e.value,
118
- })),
119
- received: payload,
120
- });
121
- return respond(200, await mod.handler(payload, token));
122
- }
123
- catch (e) {
124
- if (e instanceof AppserverError)
125
- return respond(e.status, {
126
- error: e.message,
127
- code: e.code,
128
- payload: e.payload
129
- });
130
- console.error("Unhandled server error:", e);
131
- return respond(500, {
132
- error: 'Internal server error',
133
- code: 'INTERNAL_SERVERERROR'
134
- });
135
- }
189
+ }
190
+ class AppserverAuthHandledError extends AppserverHandledError {
191
+ constructor(code, message, recoverable = false) {
192
+ super(code, message, recoverable ? 401 : 403, {});
193
+ }
194
+ }
195
+ exports.AppserverAuthHandledError = AppserverAuthHandledError;
196
+ class AppserverUserHandledError extends AppserverHandledError {
197
+ constructor(code, message, details) {
198
+ super(code, message, 400, details ?? {});
136
199
  }
137
200
  }
138
- exports.AppserverDispatcher = AppserverDispatcher;
201
+ exports.AppserverUserHandledError = AppserverUserHandledError;
@@ -6,5 +6,6 @@ export type LoaderProps<T> = {
6
6
  data: T;
7
7
  }) => React.ReactNode;
8
8
  loader?: React.ReactNode;
9
+ showRefreshing?: boolean;
9
10
  };
10
11
  export default function Loader<T>(props: LoaderProps<T>): React.JSX.Element;
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.default = Loader;
4
4
  const jsx_runtime_1 = require("react/jsx-runtime");
5
5
  const react_whispr_1 = require("../react-whispr");
6
+ const react_1 = require("react");
6
7
  const spinKeyframes = `
7
8
  @keyframes spin {
8
9
  from { transform: rotate(0deg); }
@@ -18,7 +19,18 @@ if (typeof document !== 'undefined') {
18
19
  }
19
20
  }
20
21
  function Loader(props) {
21
- const data = (0, react_whispr_1.useWhisprValue)(props.data.data);
22
+ const prev = (0, react_1.useRef)({ loading: true, progress: 0 });
23
+ const state = (0, react_whispr_1.useWhisprValue)(props.data.data);
24
+ (0, react_1.useEffect)(() => {
25
+ prev.current = state;
26
+ }, [state]);
27
+ const data = (0, react_1.useMemo)(() => {
28
+ if (props.showRefreshing)
29
+ return state;
30
+ if (state.loading && prev.current !== null)
31
+ return prev.current;
32
+ return state;
33
+ }, [state.loading, prev.current, props.showRefreshing]);
22
34
  return ((0, jsx_runtime_1.jsx)("div", { children: (0, jsx_runtime_1.jsx)(Content, { data: data, children: props.children, loader: props.loader }) }));
23
35
  }
24
36
  function Content({ data, children, loader }) {
@@ -66,7 +66,7 @@ export declare function useSynced<T extends any>(def: T, value: T | undefined, s
66
66
  *
67
67
  * @param f The async function to call. It should return a promise that resolves to the data. It is not reactive.
68
68
  * @param data The data to give to f. It must be stable, as anything in the dependency array of the useEffect and similars in the react ecosystem. If null, this function will act like an useEffect with an empty dependency array.
69
- * @param debouce Debounce time in ms. Default to 200ms. The async function will not be called if this time has not passed since the useAsync first invocation or value change. If another change happens during the wait, the first function call is never executed.
69
+ * @param debouce Debounce time in ms. Default to 200ms. The async function will not be called if this time has not passed since the useAsync first invocation or value change. If another change happens during the wait, the first function call is never executed. Not reactive
70
70
  * @returns A Dispatcher object containing:
71
71
  * - `data`: A Whispr<DispatcherStatePayload<O>> that contains the loading state, progress, and either the result data or error
72
72
  * - `filtered`: A Whispr<O | null> that contains the result data when successful, or null when loading or on error
@@ -87,7 +87,18 @@ export declare function useSynced<T extends any>(def: T, value: T | undefined, s
87
87
  * console.error('Error:', state.error);
88
88
  * }
89
89
  */
90
- export declare function useAsync<I, O>(f: (input: I, setProgress: (p: number) => void, signal: AbortSignal) => Promise<O>, data: I, debouce?: number): Dispatcher<I, O>;
90
+ export declare function useAsync<I, O>(f: (input: I, setProgress: (p: number) => void, signal: AbortSignal) => Promise<O>, data: I, debouce?: number): Dispatcher<any, O>;
91
+ /**
92
+ * Like useAsync, but it also automatically re-execute the async function periodically
93
+ *
94
+ * @param f @see useAsync
95
+ * @param data @see useAsync
96
+ * @param interval The interval to re-execute the async function. If null, the async function will not be re-executed.
97
+ * @param debounce @see useAsync
98
+ *
99
+ * @returns @see useAsync
100
+ */
101
+ export declare function useAsyncPeriodic<I, O>(f: (input: I, setProgress: (p: number) => void, signal: AbortSignal) => Promise<O>, data: I, interval: number, debounce?: number): Dispatcher<any, O>;
91
102
  /**
92
103
  * Async version of useEffect with debouncing. Executes an async function as a side effect when data changes.
93
104
  *
@@ -8,6 +8,7 @@ exports.useClock = useClock;
8
8
  exports.useDebounced = useDebounced;
9
9
  exports.useSynced = useSynced;
10
10
  exports.useAsync = useAsync;
11
+ exports.useAsyncPeriodic = useAsyncPeriodic;
11
12
  exports.useAsyncEffect = useAsyncEffect;
12
13
  exports.useRelTime = useRelTime;
13
14
  exports.useSearcher = useSearcher;
@@ -162,7 +163,7 @@ function useSynced(def, value, setValue) {
162
163
  *
163
164
  * @param f The async function to call. It should return a promise that resolves to the data. It is not reactive.
164
165
  * @param data The data to give to f. It must be stable, as anything in the dependency array of the useEffect and similars in the react ecosystem. If null, this function will act like an useEffect with an empty dependency array.
165
- * @param debouce Debounce time in ms. Default to 200ms. The async function will not be called if this time has not passed since the useAsync first invocation or value change. If another change happens during the wait, the first function call is never executed.
166
+ * @param debouce Debounce time in ms. Default to 200ms. The async function will not be called if this time has not passed since the useAsync first invocation or value change. If another change happens during the wait, the first function call is never executed. Not reactive
166
167
  * @returns A Dispatcher object containing:
167
168
  * - `data`: A Whispr<DispatcherStatePayload<O>> that contains the loading state, progress, and either the result data or error
168
169
  * - `filtered`: A Whispr<O | null> that contains the result data when successful, or null when loading or on error
@@ -196,6 +197,26 @@ function useAsync(f, data, debouce = 200) {
196
197
  // Returning dispatcher
197
198
  return dispatcher;
198
199
  }
200
+ /**
201
+ * Like useAsync, but it also automatically re-execute the async function periodically
202
+ *
203
+ * @param f @see useAsync
204
+ * @param data @see useAsync
205
+ * @param interval The interval to re-execute the async function. If null, the async function will not be re-executed.
206
+ * @param debounce @see useAsync
207
+ *
208
+ * @returns @see useAsync
209
+ */
210
+ function useAsyncPeriodic(f, data, interval, debounce) {
211
+ const timer = useClock(interval);
212
+ const bundle = (0, react_1.useMemo)(() => {
213
+ return {
214
+ data,
215
+ interval
216
+ };
217
+ }, [data, timer, debounce]);
218
+ return useAsync((i, p, s) => f(i.data, p, s), bundle, debounce);
219
+ }
199
220
  /**
200
221
  * Async version of useEffect with debouncing. Executes an async function as a side effect when data changes.
201
222
  *
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cripty2001/utils",
3
- "version": "0.0.244",
3
+ "version": "0.0.246",
4
4
  "description": "Internal Set of utils. If you need them use them, otherwise go to the next package ;)",
5
5
  "homepage": "https://github.com/cripty2001/utils#readme",
6
6
  "bugs": {
@@ -31,12 +31,9 @@
31
31
  "dependencies": {
32
32
  "@cripty2001/whispr": "^0.4.1",
33
33
  "@sinclair/typebox": "^0.34.41",
34
- "cors": "^2.8.6",
35
34
  "lodash": "^4.17.21"
36
35
  },
37
36
  "devDependencies": {
38
- "@types/cors": "^2.8.19",
39
- "@types/express": "^5.0.5",
40
37
  "@types/lodash": "^4.17.21",
41
38
  "@types/node": "^24.13.3",
42
39
  "@types/react": "^19",
@@ -44,9 +41,10 @@
44
41
  "typescript": "^7.0.2"
45
42
  },
46
43
  "peerDependencies": {
47
- "@msgpack/msgpack": "^3.1.2",
44
+ "@fastify/type-provider-typebox": "^6.1.0",
48
45
  "dotenv": "^17.2.3",
49
- "express": "^5.2.1",
46
+ "fastify": "^5.12.5",
47
+ "jose": "^6.2.12",
50
48
  "lucide-react": ">=0.552.0",
51
49
  "react": "^19"
52
50
  },
@@ -96,11 +94,6 @@
96
94
  "types": "./dist/Appserver/server.d.ts",
97
95
  "require": "./dist/Appserver/server.js"
98
96
  },
99
- "./appserver/client": {
100
- "import": "./dist/Appserver/client.js",
101
- "types": "./dist/Appserver/client.d.ts",
102
- "require": "./dist/Appserver/client.js"
103
- },
104
97
  "./react": {
105
98
  "import": "./dist/react/index.js",
106
99
  "types": "./dist/react/index.d.ts",