@cripty2001/utils 0.0.243 → 0.0.245

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;
package/dist/index.d.ts CHANGED
@@ -44,7 +44,7 @@ export declare function getRandomOtp(length?: number, char?: boolean): string;
44
44
  * @param ms The number of milliseconds to sleep
45
45
  */
46
46
  export declare function sleep(ms: number): Promise<void>;
47
- export declare function parseHash(fields: string[]): URLSearchParams;
47
+ export declare function parseHash(fields: string[], deleteFields?: boolean): URLSearchParams;
48
48
  /**
49
49
  * Start an infinite loop executing an async callback spaced by at least the given interval.
50
50
  * The loop ignores errors (the next execution is not affected), but an optional error callback can be provided to handle them.
package/dist/index.js CHANGED
@@ -96,7 +96,7 @@ function getRandomOtp(length = 6, char = false) {
96
96
  function sleep(ms) {
97
97
  return new Promise((resolve) => setTimeout(resolve, ms));
98
98
  }
99
- function parseHash(fields) {
99
+ function parseHash(fields, deleteFields = false) {
100
100
  // Checking empty hash
101
101
  if (window.location.hash === "")
102
102
  return new URLSearchParams();
@@ -107,7 +107,9 @@ function parseHash(fields) {
107
107
  for (const field of fields) {
108
108
  if (data.has(field)) {
109
109
  toReturn.set(field, data.get(field));
110
- data.delete(field);
110
+ if (deleteFields) {
111
+ data.delete(field);
112
+ }
111
113
  }
112
114
  }
113
115
  // Reencoding hash without extracted fields
@@ -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 }) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cripty2001/utils",
3
- "version": "0.0.243",
3
+ "version": "0.0.245",
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",