hapii-js 0.0.0-stage → 2.0.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/lib/toolkit.js ADDED
@@ -0,0 +1,258 @@
1
+ 'use strict';
2
+
3
+ const Boom = require('@hapi/boom');
4
+ const Bounce = require('@hapi/bounce');
5
+ const Hoek = require('@hapi/hoek');
6
+
7
+
8
+ const internals = {};
9
+
10
+
11
+ exports.reserved = [
12
+ 'abandon',
13
+ 'authenticated',
14
+ 'close',
15
+ 'context',
16
+ 'continue',
17
+ 'entity',
18
+ 'redirect',
19
+ 'realm',
20
+ 'request',
21
+ 'response',
22
+ 'state',
23
+ 'unauthenticated',
24
+ 'unstate'
25
+ ];
26
+
27
+
28
+ exports.symbols = {
29
+ abandon: Symbol('abandon'),
30
+ close: Symbol('close'),
31
+ continue: Symbol('continue')
32
+ };
33
+
34
+
35
+ exports.Manager = class {
36
+
37
+ constructor() {
38
+
39
+ this._toolkit = internals.toolkit();
40
+ }
41
+
42
+ async execute(method, request, options) {
43
+
44
+ const h = new this._toolkit(request, options);
45
+ const bind = options.bind ?? null;
46
+
47
+ try {
48
+ let operation;
49
+
50
+ if (bind) {
51
+ operation = method.call(bind, request, h);
52
+ }
53
+ else if (options.args) {
54
+ operation = method(request, h, ...options.args);
55
+ }
56
+ else {
57
+ operation = method(request, h);
58
+ }
59
+
60
+ var response = await exports.timed(operation, options);
61
+ }
62
+ catch (err) {
63
+ if (Bounce.isSystem(err)) {
64
+ response = Boom.badImplementation(err);
65
+ }
66
+ else if (!Bounce.isError(err)) {
67
+ response = Boom.badImplementation('Cannot throw non-error object', err);
68
+ }
69
+ else {
70
+ response = Boom.boomify(err);
71
+ }
72
+ }
73
+
74
+ // Process response
75
+
76
+ if (options.ignoreResponse) {
77
+ return response;
78
+ }
79
+
80
+ if (response === undefined) {
81
+ response = Boom.badImplementation(`${method.name} method did not return a value, a promise, or throw an error`);
82
+ }
83
+
84
+ if (options.continue &&
85
+ response === exports.symbols.continue) {
86
+
87
+ if (options.continue === 'undefined') {
88
+ return;
89
+ }
90
+
91
+ // 'null'
92
+
93
+ response = null;
94
+ }
95
+
96
+ if (options.auth &&
97
+ response instanceof internals.Auth) {
98
+
99
+ return response;
100
+ }
101
+
102
+ if (typeof response !== 'symbol') {
103
+ response = request._core.Response.wrap(response, request);
104
+ if (!response.isBoom && response._state === 'init') {
105
+ await response._prepare();
106
+ }
107
+ }
108
+
109
+ return response;
110
+ }
111
+
112
+ decorate(name, method) {
113
+
114
+ this._toolkit.prototype[name] = method;
115
+ }
116
+
117
+ async failAction(request, failAction, err, options) {
118
+
119
+ const retain = options.retain ? err : undefined;
120
+ if (failAction === 'ignore') {
121
+ return retain;
122
+ }
123
+
124
+ if (failAction === 'log') {
125
+ request._log(options.tags, err);
126
+ return retain;
127
+ }
128
+
129
+ if (failAction === 'error') {
130
+ throw err;
131
+ }
132
+
133
+ return await this.execute(failAction, request, { realm: request.route.realm, args: [options.details ?? err] });
134
+ }
135
+ };
136
+
137
+
138
+ exports.timed = async function (method, options) {
139
+
140
+ if (!options.timeout) {
141
+ return method;
142
+ }
143
+
144
+ const timer = new Promise((resolve, reject) => {
145
+
146
+ const handler = () => {
147
+
148
+ reject(Boom.internal(`${options.name} timed out`));
149
+ };
150
+
151
+ setTimeout(handler, options.timeout);
152
+ });
153
+
154
+ return await Promise.race([timer, method]);
155
+ };
156
+
157
+
158
+ /*
159
+ const handler = function (request, h) {
160
+
161
+ result / h.response(result) -> result // Not allowed before handler
162
+ h.response(result).takeover() -> result (respond)
163
+ h.continue -> null // Defaults to null only in handler and pre, not allowed in auth
164
+
165
+ throw error / h.response(error) -> error (respond) // failAction override in pre
166
+ <undefined> -> badImplementation (respond)
167
+
168
+ // Auth only (scheme.payload and scheme.response use the same interface as pre-handler extension methods)
169
+
170
+ h.unauthenticated(error, data) -> error (respond) + data
171
+ h.authenticated(data ) -> (continue) + data
172
+ };
173
+ */
174
+
175
+ internals.toolkit = function () {
176
+
177
+ const Toolkit = class {
178
+
179
+ constructor(request, options) {
180
+
181
+ this.context = options.bind;
182
+ this.realm = options.realm;
183
+ this.request = request;
184
+
185
+ this._auth = options.auth;
186
+ }
187
+
188
+ response(result) {
189
+
190
+ Hoek.assert(!result || typeof result !== 'object' || typeof result.then !== 'function', 'Cannot wrap a promise');
191
+ Hoek.assert(result instanceof Error === false, 'Cannot wrap an error');
192
+ Hoek.assert(typeof result !== 'symbol', 'Cannot wrap a symbol');
193
+
194
+ return this.request._core.Response.wrap(result, this.request);
195
+ }
196
+
197
+ redirect(location) {
198
+
199
+ return this.response('').redirect(location);
200
+ }
201
+
202
+ entity(options) {
203
+
204
+ Hoek.assert(options, 'Entity method missing required options');
205
+ Hoek.assert(options.etag || options.modified, 'Entity methods missing required options key');
206
+
207
+ this.request._entity = options;
208
+
209
+ const entity = this.request._core.Response.entity(options.etag, options);
210
+ if (this.request._core.Response.unmodified(this.request, entity)) {
211
+ return this.response().code(304).takeover();
212
+ }
213
+ }
214
+
215
+ state(name, value, options) {
216
+
217
+ this.request._setState(name, value, options);
218
+ }
219
+
220
+ unstate(name, options) {
221
+
222
+ this.request._clearState(name, options);
223
+ }
224
+
225
+ authenticated(data) {
226
+
227
+ Hoek.assert(this._auth, 'Method not supported outside of authentication');
228
+ Hoek.assert(data?.credentials, 'Authentication data missing credentials information');
229
+
230
+ return new internals.Auth(null, data);
231
+ }
232
+
233
+ unauthenticated(error, data) {
234
+
235
+ Hoek.assert(this._auth, 'Method not supported outside of authentication');
236
+ Hoek.assert(!data || data.credentials, 'Authentication data missing credentials information');
237
+
238
+ return new internals.Auth(error, data);
239
+ }
240
+ };
241
+
242
+ Toolkit.prototype.abandon = exports.symbols.abandon;
243
+ Toolkit.prototype.close = exports.symbols.close;
244
+ Toolkit.prototype.continue = exports.symbols.continue;
245
+
246
+ return Toolkit;
247
+ };
248
+
249
+
250
+ internals.Auth = class {
251
+
252
+ constructor(error, data) {
253
+
254
+ this.isAuth = true;
255
+ this.error = error;
256
+ this.data = data;
257
+ }
258
+ };
@@ -0,0 +1,388 @@
1
+ 'use strict';
2
+
3
+ const Http = require('http');
4
+
5
+ const Ammo = require('@hapi/ammo');
6
+ const Boom = require('@hapi/boom');
7
+ const Bounce = require('@hapi/bounce');
8
+ const Hoek = require('@hapi/hoek');
9
+ const Teamwork = require('@hapi/teamwork');
10
+
11
+ const Config = require('./config');
12
+
13
+
14
+ const internals = {};
15
+
16
+
17
+ exports.send = async function (request) {
18
+
19
+ const response = request.response;
20
+
21
+ try {
22
+ if (response.isBoom) {
23
+ await internals.fail(request, response);
24
+ return;
25
+ }
26
+
27
+ await internals.marshal(response);
28
+ await internals.transmit(response);
29
+ }
30
+ catch (err) {
31
+ Bounce.rethrow(err, 'system');
32
+ request._setResponse(err);
33
+ return internals.fail(request, err);
34
+ }
35
+ };
36
+
37
+
38
+ internals.marshal = async function (response) {
39
+
40
+ for (const func of response.request._route._marshalCycle) {
41
+ await func(response);
42
+ }
43
+ };
44
+
45
+
46
+ internals.fail = async function (request, boom) {
47
+
48
+ const response = internals.error(request, boom);
49
+ request.response = response; // Not using request._setResponse() to avoid double log
50
+
51
+ try {
52
+ await internals.marshal(response);
53
+ }
54
+ catch (err) {
55
+ Bounce.rethrow(err, 'system');
56
+
57
+ // Failed to marshal an error - replace with minimal representation of original error
58
+
59
+ const minimal = {
60
+ statusCode: response.statusCode,
61
+ error: Http.STATUS_CODES[response.statusCode],
62
+ message: boom.message
63
+ };
64
+
65
+ response._payload = new request._core.Response.Payload(JSON.stringify(minimal), {});
66
+ }
67
+
68
+ return internals.transmit(response);
69
+ };
70
+
71
+
72
+ internals.error = function (request, boom) {
73
+
74
+ const error = boom.output;
75
+ const response = new request._core.Response(error.payload, request, { error: boom });
76
+ response.code(error.statusCode);
77
+ response.headers = Hoek.clone(error.headers); // Prevent source from being modified
78
+ return response;
79
+ };
80
+
81
+
82
+ internals.transmit = function (response) {
83
+
84
+ const request = response.request;
85
+ const length = internals.length(response);
86
+
87
+ // Pipes
88
+
89
+ const encoding = request._core.compression.encoding(response, length);
90
+ const ranger = encoding ? null : internals.range(response, length);
91
+ const compressor = internals.encoding(response, encoding);
92
+
93
+ // Connection: close
94
+
95
+ const isInjection = request.isInjected;
96
+ if (!(isInjection || request._core.started) ||
97
+ request._isPayloadPending && !request.raw.req._readableState.ended) {
98
+
99
+ response._header('connection', 'close');
100
+ }
101
+
102
+ // Write headers
103
+
104
+ internals.writeHead(response);
105
+
106
+ // Injection
107
+
108
+ if (isInjection) {
109
+ request.raw.res[Config.symbol] = { request };
110
+
111
+ if (response.variety === 'plain') {
112
+ request.raw.res[Config.symbol].result = response._isPayloadSupported() ? response.source : null;
113
+ }
114
+ }
115
+
116
+ // Finalize response stream
117
+
118
+ const stream = internals.chain([response._payload, response._tap(), compressor, ranger]);
119
+ return internals.pipe(request, stream);
120
+ };
121
+
122
+
123
+ internals.length = function (response) {
124
+
125
+ const request = response.request;
126
+
127
+ const header = response.headers['content-length'];
128
+ if (header === undefined) {
129
+ return null;
130
+ }
131
+
132
+ let length = header;
133
+ if (typeof length === 'string') {
134
+ length = parseInt(header, 10);
135
+ if (!isFinite(length)) {
136
+ delete response.headers['content-length'];
137
+ return null;
138
+ }
139
+ }
140
+
141
+ // Empty response
142
+
143
+ if (length === 0 &&
144
+ !response._statusCode &&
145
+ response.statusCode === 200 &&
146
+ request.route.settings.response.emptyStatusCode !== 200) {
147
+
148
+ response.code(204);
149
+ delete response.headers['content-length'];
150
+ }
151
+
152
+ return length;
153
+ };
154
+
155
+
156
+ internals.range = function (response, length) {
157
+
158
+ const request = response.request;
159
+
160
+ if (!length ||
161
+ !request.route.settings.response.ranges ||
162
+ request.method !== 'get' ||
163
+ response.statusCode !== 200) {
164
+
165
+ return null;
166
+ }
167
+
168
+ response._header('accept-ranges', 'bytes');
169
+
170
+ if (!request.headers.range) {
171
+ return null;
172
+ }
173
+
174
+ // Check If-Range
175
+
176
+ if (request.headers['if-range'] &&
177
+ request.headers['if-range'] !== response.headers.etag) { // Ignoring last-modified date (weak)
178
+
179
+ return null;
180
+ }
181
+
182
+ // Parse header
183
+
184
+ const ranges = Ammo.header(request.headers.range, length);
185
+ if (!ranges) {
186
+ const error = Boom.rangeNotSatisfiable();
187
+ error.output.headers['content-range'] = 'bytes */' + length;
188
+ throw error;
189
+ }
190
+
191
+ // Prepare transform
192
+
193
+ if (ranges.length !== 1) { // Ignore requests for multiple ranges
194
+ return null;
195
+ }
196
+
197
+ const range = ranges[0];
198
+ response.code(206);
199
+ response.bytes(range.to - range.from + 1);
200
+ response._header('content-range', 'bytes ' + range.from + '-' + range.to + '/' + length);
201
+
202
+ return new Ammo.Clip(range);
203
+ };
204
+
205
+
206
+ internals.encoding = function (response, encoding) {
207
+
208
+ const request = response.request;
209
+
210
+ const header = response.headers['content-encoding'] || encoding;
211
+ if (header &&
212
+ response.headers.etag &&
213
+ response.settings.varyEtag) {
214
+
215
+ response.headers.etag = response.headers.etag.slice(0, -1) + '-' + header + '"';
216
+ }
217
+
218
+ if (!encoding ||
219
+ response.statusCode === 206 ||
220
+ !response._isPayloadSupported()) {
221
+
222
+ return null;
223
+ }
224
+
225
+ delete response.headers['content-length'];
226
+ response._header('content-encoding', encoding);
227
+ const compressor = request._core.compression.encoder(request, encoding);
228
+ if (response.variety === 'stream' &&
229
+ typeof response._payload.setCompressor === 'function') {
230
+
231
+ response._payload.setCompressor(compressor);
232
+ }
233
+
234
+ return compressor;
235
+ };
236
+
237
+
238
+ internals.pipe = function (request, stream) {
239
+
240
+ const team = new Teamwork.Team();
241
+
242
+ // Write payload
243
+
244
+ const env = { stream, request, team };
245
+
246
+ if (request._closed) {
247
+
248
+ // The request has already been aborted - no need to wait or attempt to write.
249
+
250
+ internals.end(env, 'aborted');
251
+ return team.work;
252
+ }
253
+
254
+ const aborted = internals.end.bind(null, env, 'aborted');
255
+ const close = internals.end.bind(null, env, 'close');
256
+ const end = internals.end.bind(null, env, null);
257
+
258
+ request.raw.req.on('aborted', aborted);
259
+
260
+ request.raw.res.on('close', close);
261
+ request.raw.res.on('error', end);
262
+ request.raw.res.on('finish', end);
263
+
264
+ if (stream.writeToStream) {
265
+ stream.writeToStream(request.raw.res);
266
+ }
267
+ else {
268
+ stream.on('error', end);
269
+ stream.on('close', aborted);
270
+ stream.pipe(request.raw.res);
271
+ }
272
+
273
+ return team.work;
274
+ };
275
+
276
+
277
+ internals.end = function (env, event, err) {
278
+
279
+ const { request, stream, team } = env;
280
+
281
+ if (!team) { // Used instead of cleaning up emitter listeners
282
+ return;
283
+ }
284
+
285
+ env.team = null;
286
+
287
+ if (request.raw.res.writableEnded) {
288
+ request.info.responded = Date.now();
289
+
290
+ team.attend();
291
+ return;
292
+ }
293
+
294
+ if (err) {
295
+ request.raw.res.destroy();
296
+ request._core.Response.drain(stream);
297
+ }
298
+
299
+ // Update reported response to reflect the error condition
300
+
301
+ const origResponse = request.response;
302
+ const error = err ? Boom.boomify(err) :
303
+ new Boom.Boom(`Request ${event}`, { statusCode: request.route.settings.response.disconnectStatusCode, data: origResponse });
304
+
305
+ request._setResponse(error);
306
+
307
+ // Make inject throw a disconnect error
308
+
309
+ if (request.raw.res[Config.symbol]) {
310
+ request.raw.res[Config.symbol].error = event ? error :
311
+ new Boom.Boom(`Response error`, { statusCode: request.route.settings.response.disconnectStatusCode, data: origResponse });
312
+ }
313
+
314
+ if (event) {
315
+ request._log(['response', 'error', event]);
316
+ }
317
+ else {
318
+ request._log(['response', 'error'], err);
319
+ }
320
+
321
+ request.raw.res.end(); // Triggers injection promise resolve
322
+ team.attend();
323
+ };
324
+
325
+
326
+ internals.writeHead = function (response) {
327
+
328
+ const res = response.request.raw.res;
329
+ const headers = Object.keys(response.headers);
330
+ let i = 0;
331
+
332
+ try {
333
+ for (; i < headers.length; ++i) {
334
+ const header = headers[i];
335
+ const value = response.headers[header];
336
+ if (value !== undefined) {
337
+ res.setHeader(header, value);
338
+ }
339
+ }
340
+ }
341
+ catch (err) {
342
+ for (--i; i >= 0; --i) {
343
+ res.removeHeader(headers[i]); // Undo headers
344
+ }
345
+
346
+ throw Boom.boomify(err);
347
+ }
348
+
349
+ if (response.settings.message) {
350
+ res.statusMessage = response.settings.message;
351
+ }
352
+
353
+ try {
354
+ res.writeHead(response.statusCode);
355
+ }
356
+ catch (err) {
357
+ throw Boom.boomify(err);
358
+ }
359
+ };
360
+
361
+
362
+ internals.chain = function (sources) {
363
+
364
+ let from = sources[0];
365
+ for (let i = 1; i < sources.length; ++i) {
366
+ const to = sources[i];
367
+ if (to) {
368
+ from.on('close', internals.destroyPipe.bind(from, to));
369
+ from.on('error', internals.errorPipe.bind(from, to));
370
+ from = from.pipe(to);
371
+ }
372
+ }
373
+
374
+ return from;
375
+ };
376
+
377
+
378
+ internals.destroyPipe = function (to) {
379
+
380
+ if (!this.readableEnded && !this.errored) {
381
+ to.destroy();
382
+ }
383
+ };
384
+
385
+ internals.errorPipe = function (to, err) {
386
+
387
+ to.emit('error', err);
388
+ };
@@ -0,0 +1,21 @@
1
+ // Definitions adapted from DefinitelyTyped, originally created by:
2
+ // Rafael Souza Fijalkowski <https://github.com/rafaelsouzaf>
3
+ // Justin Simms <https://github.com/jhsimms>
4
+ // Simon Schick <https://github.com/SimonSchick>
5
+ // Rodrigo Saboya <https://github.com/saboya>
6
+ // Silas Rech <https://github.com/lenovouser>
7
+
8
+ export * from './plugin';
9
+ export * from './response';
10
+ export * from './request';
11
+ export * from './route';
12
+ export * from './server';
13
+ export * from './utils';
14
+
15
+ // Kept for backwards compatibility only (remove in next major)
16
+
17
+ export namespace Utils {
18
+ interface Dictionary<T> {
19
+ [key: string]: T;
20
+ }
21
+ }