@docbox-nz/hapi-gateway 0.1.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.
Files changed (45) hide show
  1. package/.eslintignore +6 -0
  2. package/.eslintrc +131 -0
  3. package/.prettierignore +11 -0
  4. package/.prettierrc +6 -0
  5. package/LICENSE.md +21 -0
  6. package/README.md +152 -0
  7. package/cspell.json +6 -0
  8. package/dist/api/adminService.d.ts +16 -0
  9. package/dist/api/boxService.d.ts +43 -0
  10. package/dist/api/client.d.ts +45 -0
  11. package/dist/api/docboxFile.d.ts +15 -0
  12. package/dist/api/fileService.d.ts +251 -0
  13. package/dist/api/folderService.d.ts +45 -0
  14. package/dist/api/linkService.d.ts +115 -0
  15. package/dist/api/taskService.d.ts +23 -0
  16. package/dist/api/utils.d.ts +1 -0
  17. package/dist/error.d.ts +11 -0
  18. package/dist/index.browser.cjs +988 -0
  19. package/dist/index.browser.cjs.map +1 -0
  20. package/dist/index.browser.esm.js +984 -0
  21. package/dist/index.browser.esm.js.map +1 -0
  22. package/dist/index.browser.js +4390 -0
  23. package/dist/index.browser.js.map +1 -0
  24. package/dist/index.d.ts +4 -0
  25. package/dist/index.node.cjs +22660 -0
  26. package/dist/index.node.cjs.map +1 -0
  27. package/dist/index.node.esm.js +22657 -0
  28. package/dist/index.node.esm.js.map +1 -0
  29. package/dist/index.node.js +20809 -0
  30. package/dist/index.node.js.map +1 -0
  31. package/dist/options.d.ts +79 -0
  32. package/dist/types/box.d.ts +59 -0
  33. package/dist/types/file.d.ts +371 -0
  34. package/dist/types/folder.d.ts +153 -0
  35. package/dist/types/index.d.ts +22 -0
  36. package/dist/types/link.d.ts +136 -0
  37. package/dist/types/search.d.ts +177 -0
  38. package/dist/types/shared.d.ts +94 -0
  39. package/dist/types/user.d.ts +20 -0
  40. package/package.json +56 -0
  41. package/rollup.config.js +36 -0
  42. package/src/error.ts +46 -0
  43. package/src/index.ts +244 -0
  44. package/src/options.ts +95 -0
  45. package/tsconfig.json +15 -0
package/src/index.ts ADDED
@@ -0,0 +1,244 @@
1
+ import { badRequest, forbidden } from '@hapi/boom';
2
+ import type { Plugin, Request, ResponseToolkit, RouteOptions, ServerRoute } from '@hapi/hapi';
3
+ import axios from 'axios';
4
+ import type { AxiosInstance } from 'axios';
5
+ import { createHandleAxiosError } from './error';
6
+ import type { DocboxRequestTenant, DocboxRequestUser, PluginOptions } from './options';
7
+
8
+ const DEFAULT_BASE_PATH: string = '/';
9
+
10
+ export const DocboxGateway: Plugin<PluginOptions> = {
11
+ name: '@docbox-nz/hapi-gateway',
12
+ version: '0.1.0',
13
+ register: function (server, options) {
14
+ const routes = createDocboxRoutes(options);
15
+ for (const route of routes) {
16
+ server.route(route);
17
+ }
18
+ },
19
+ };
20
+
21
+ export function createDocboxRoutes(options: PluginOptions) {
22
+ let basePath = options.basePath ?? DEFAULT_BASE_PATH;
23
+
24
+ // Remove trailing slashes from base path
25
+ if (basePath.endsWith('/')) basePath = basePath.substring(0, basePath.length - 1);
26
+
27
+ const baseRouteOptions: RouteOptions = options.baseRouteOptions ?? {};
28
+ const baseForwardRouteOptions: RouteOptions = options.baseForwardRouteOptions ?? {};
29
+
30
+ // Axios instance to perform connections
31
+ const axiosInstance = axios.create({
32
+ ...options.axiosConfig,
33
+ baseURL: options.docboxBaseURL,
34
+ headers: {
35
+ 'Content-Type': 'application/json',
36
+ Accept: 'application/json',
37
+ },
38
+ });
39
+
40
+ // Add interceptor to handle axios errors
41
+ axiosInstance.interceptors.response.use(
42
+ (res) => res,
43
+ createHandleAxiosError(options.docboxBaseURL)
44
+ );
45
+
46
+ const getRequestUser: PluginOptions['getRequestUser'] = options.getRequestUser ?? (() => null);
47
+
48
+ // Base route for creating document boxes
49
+ const createBoxRoute: ServerRoute = {
50
+ path: `${basePath}/box`,
51
+ method: 'POST',
52
+ handler: async function (request, h) {
53
+ const payload = request.payload;
54
+ if (typeof payload !== 'object') {
55
+ return badRequest('request payload must be an object');
56
+ }
57
+
58
+ const scope = (payload as any).scope;
59
+ if (scope === undefined || typeof scope !== 'string') {
60
+ return badRequest('scope must be defined and a string');
61
+ }
62
+
63
+ const path = request.path.substring(basePath.length);
64
+ const isAllowed = await options.isAllowedWrite(request, scope, path);
65
+ if (!isAllowed) {
66
+ return forbidden("you don't have permission to access this resource");
67
+ }
68
+
69
+ const tenant = await options.getRequestTenant(request);
70
+ const user = await getRequestUser(request);
71
+ return forwardRequest(axiosInstance, request, tenant, user, h, path);
72
+ },
73
+ options: baseRouteOptions,
74
+ };
75
+
76
+ // Docbox read forwarding endpoint
77
+ const forwardReadRoute: ServerRoute = {
78
+ path: `${basePath}/box/{scope}/{path*}`,
79
+ method: ['GET'],
80
+ handler: async function (request, h) {
81
+ const scope = request.params.scope;
82
+ if (scope === undefined || typeof scope !== 'string') {
83
+ return badRequest('scope must be defined and a string');
84
+ }
85
+
86
+ const path = request.path.substring(basePath.length);
87
+ const isAllowed = await options.isAllowedRead(request, scope, path);
88
+ if (!isAllowed) {
89
+ return forbidden("you don't have permission to access this resource");
90
+ }
91
+
92
+ const tenant = await options.getRequestTenant(request);
93
+ const user = await getRequestUser(request);
94
+ return forwardRequest(axiosInstance, request, tenant, user, h, path);
95
+ },
96
+ options: { ...baseRouteOptions, ...baseForwardRouteOptions },
97
+ };
98
+
99
+ // Docbox write forwarding endpoint
100
+ const forwardWriteRoute: ServerRoute = {
101
+ path: `${basePath}/box/{scope}/{path*}`,
102
+ method: ['POST', 'PATCH', 'PUT', 'DELETE'],
103
+ handler: async function (request, h) {
104
+ const scope = request.params.scope;
105
+ if (scope === undefined || typeof scope !== 'string') {
106
+ return badRequest('scope must be defined and a string');
107
+ }
108
+
109
+ const path = request.path.substring(basePath.length);
110
+
111
+ // Searching uses the POST method but only requires read access
112
+ const isWriting = isWriteRequest(request, path);
113
+
114
+ const accessHandler = isWriting ? options.isAllowedWrite : options.isAllowedRead;
115
+
116
+ const isAllowed = await accessHandler(request, scope, path);
117
+ if (!isAllowed) {
118
+ return forbidden("you don't have permission to access this resource");
119
+ }
120
+
121
+ const tenant = await options.getRequestTenant(request);
122
+ const user = await getRequestUser(request);
123
+ return forwardRequest(axiosInstance, request, tenant, user, h, path);
124
+ },
125
+ options: {
126
+ ...baseRouteOptions,
127
+ ...baseForwardRouteOptions,
128
+ payload: {
129
+ ...baseRouteOptions.payload,
130
+ ...baseForwardRouteOptions.payload,
131
+
132
+ // Payload must not be parsed so that files are properly sent to
133
+ // the docbox server
134
+ parse: false,
135
+
136
+ // Ensure we get our data as full buffers to forward onward, not
137
+ // preprocessed by HAPI
138
+ output: 'data',
139
+ },
140
+ },
141
+ };
142
+
143
+ return [createBoxRoute, forwardReadRoute, forwardWriteRoute];
144
+ }
145
+
146
+ /**
147
+ * Additional check for write endpoints to see if the
148
+ * request is actually a write, as some endpoints such
149
+ * as search use the POST method
150
+ *
151
+ * @param request The HTTP request
152
+ * @param path The request path
153
+ * @returns Whether the request is a write request
154
+ */
155
+ function isWriteRequest(request: Request, path: string) {
156
+ if (request.method === 'post') {
157
+ const parts = path.substring(1).split('/');
158
+
159
+ // Request is a search request (/box/{scope}/search)
160
+ if (parts.length === 3 && parts[0] === 'box' && parts[2] === 'search') {
161
+ return false;
162
+ }
163
+
164
+ // Request is a file search request (/box/{scope}/file/{file_id}/search)
165
+ if (parts.length === 5 && parts[0] === 'box' && parts[2] === 'file' && parts[4] === 'search') {
166
+ return false;
167
+ }
168
+ }
169
+
170
+ return true;
171
+ }
172
+
173
+ /**
174
+ * Forwards the provided request onto the docbox server
175
+ *
176
+ * @param axios Axios instance to forward the request through
177
+ * @param request The request itself
178
+ * @param user User performing the request
179
+ * @param h Response toolkit for creating the response
180
+ * @param path Path requested from the server
181
+ */
182
+ async function forwardRequest(
183
+ axios: AxiosInstance,
184
+ request: Request,
185
+ tenant: DocboxRequestTenant,
186
+ user: DocboxRequestUser | null,
187
+ h: ResponseToolkit,
188
+ path: string
189
+ ) {
190
+ const userHeaders: Record<string, any> = request.headers;
191
+ const forwardHeaders: Record<string, any> = {};
192
+
193
+ // Forward content related headers
194
+ if (userHeaders['accept'] !== undefined) {
195
+ forwardHeaders['accept'] = userHeaders['accept'];
196
+ }
197
+
198
+ if (userHeaders['content-type'] !== undefined) {
199
+ forwardHeaders['content-type'] = userHeaders['content-type'];
200
+ }
201
+
202
+ if (userHeaders['content-length'] !== undefined) {
203
+ forwardHeaders['content-length'] = userHeaders['content-length'];
204
+ }
205
+
206
+ // Set tenant headers
207
+ forwardHeaders['x-tenant-env'] = tenant.env;
208
+ forwardHeaders['x-tenant-id'] = tenant.id;
209
+
210
+ // Set user headers
211
+ if (user !== null && user.id !== undefined) {
212
+ forwardHeaders['x-user-id'] = encodeURIComponent(user.id);
213
+
214
+ if (user.name) {
215
+ forwardHeaders['x-user-name'] = encodeURIComponent(user.name);
216
+ }
217
+
218
+ if (user.imageId) {
219
+ forwardHeaders['x-user-image-id'] = encodeURIComponent(user.imageId);
220
+ }
221
+ }
222
+
223
+ // Forward the request to docbox
224
+ const docboxResponse = await axios.request({
225
+ method: request.method.toUpperCase(),
226
+ url: path,
227
+ headers: forwardHeaders,
228
+ data: request.payload,
229
+ params: request.query,
230
+ responseType: 'stream',
231
+ // Don't throw if the status code is not an 2xx status code
232
+ // (we pass error statuses onto the consumer)
233
+ validateStatus: () => true,
234
+ });
235
+
236
+ let response = h.response(docboxResponse.data).code(docboxResponse.status);
237
+
238
+ // Set the response headers
239
+ Object.entries(docboxResponse.headers).forEach(
240
+ ([name, value]) => (response = response.header(name, value))
241
+ );
242
+
243
+ return response;
244
+ }
package/src/options.ts ADDED
@@ -0,0 +1,95 @@
1
+ import type { Request, RouteOptions } from '@hapi/hapi';
2
+ import type { CreateAxiosDefaults } from 'axios';
3
+
4
+ export interface DocboxRequestUser {
5
+ /** Internal unique user ID for the user (Specific to your app) */
6
+ id?: string;
7
+
8
+ /** Username of the user */
9
+ name?: string;
10
+
11
+ /** Some internal field used to identify the profile image of the user */
12
+ imageId?: string;
13
+ }
14
+
15
+ export interface DocboxRequestTenant {
16
+ /** ID of the tenant */
17
+ id: string;
18
+
19
+ /** Environment the tenant is within */
20
+ env: string;
21
+ }
22
+
23
+ export interface PluginOptions {
24
+ /**
25
+ * Base URL of the docbox server
26
+ */
27
+ docboxBaseURL: string;
28
+
29
+ /**
30
+ * Base path to serve docbox endpoints from. Must begin
31
+ * with a slash
32
+ *
33
+ * Docbox endpoints will be added in a nested /box path
34
+ *
35
+ * @default /
36
+ */
37
+ basePath?: string;
38
+
39
+ /**
40
+ * Base set of route options applied to any routes created
41
+ * by docbox
42
+ */
43
+ baseRouteOptions?: RouteOptions;
44
+
45
+ /**
46
+ * Base set of route options applied only to forwarding
47
+ * routes created by docbox
48
+ */
49
+ baseForwardRouteOptions?: RouteOptions;
50
+
51
+ /**
52
+ * Additional configuration options for axios
53
+ */
54
+ axiosConfig?: CreateAxiosDefaults;
55
+
56
+ /**
57
+ * Write access check, will be invoked to check whether the request
58
+ * has authorization to perform a write action against docbox
59
+ *
60
+ * @param request The HTTP request
61
+ * @param scope The requested docbox scope
62
+ * @param path The requested path
63
+ * @returns Whether the user is allowed to write
64
+ */
65
+ isAllowedWrite(request: Request, scope: string, path: string): Promise<boolean> | boolean;
66
+
67
+ /**
68
+ * Read access check, will be invoked to check whether the request
69
+ * has authorization to perform a read action against docbox
70
+ *
71
+ * @param request The HTTP request
72
+ * @param scope The requested docbox scope
73
+ * @param path The requested path
74
+ * @returns Whether the user is allowed to write
75
+ */
76
+ isAllowedRead(request: Request, scope: string, path: string): Promise<boolean> | boolean;
77
+
78
+ /**
79
+ * Extract the user details from the provided request
80
+ *
81
+ * @param request The HTTP request
82
+ * @returns The user details if they were obtainable
83
+ */
84
+ getRequestUser?: (
85
+ request: Request
86
+ ) => Promise<DocboxRequestUser | null> | (DocboxRequestUser | null);
87
+
88
+ /**
89
+ * Gets the docbox tenant from the request
90
+ *
91
+ * @param request The HTTP request
92
+ * @returns The current tenant
93
+ */
94
+ getRequestTenant(request: Request): Promise<DocboxRequestTenant> | DocboxRequestTenant;
95
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,15 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES6",
4
+ "module": "ESNext",
5
+ "declaration": true,
6
+ "declarationDir": "dist/types",
7
+ "outDir": "dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "typeRoots": ["./node_modules/@types"],
12
+ "moduleResolution": "node"
13
+ },
14
+ "include": ["src"]
15
+ }