expediate 1.0.0 → 1.0.1

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/dist/apis.js DELETED
@@ -1,250 +0,0 @@
1
- /* Copyright 2021 Fabien Bavent
2
- *
3
- * Permission is hereby granted, free of charge, to any person obtaining a
4
- * copy of this software and associated documentation files (the "Software"),
5
- * to deal in the Software without restriction, including without limitation
6
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
7
- * and/or sell copies of the Software, and to permit persons to whom the
8
- * Software is furnished to do so, subject to the following conditions:
9
- *
10
- * The above copyright notice and this permission notice shall be included
11
- * in all copies or substantial portions of the Software.
12
- *
13
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14
- * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19
- * DEALINGS IN THE SOFTWARE.
20
- */
21
- 'use strict';
22
- var __importDefault = (this && this.__importDefault) || function (mod) {
23
- return (mod && mod.__esModule) ? mod : { "default": mod };
24
- };
25
- Object.defineProperty(exports, "__esModule", { value: true });
26
- exports.apiBuilder = apiBuilder;
27
- const router_js_1 = __importDefault(require("./router.js"));
28
- // ---------------------------------------------------------------------------
29
- // Internal helpers
30
- // ---------------------------------------------------------------------------
31
- /**
32
- * Instantiate a new service module for the given scope `key`.
33
- *
34
- * The lifecycle is:
35
- * 1. **`data(key)`** — create the initial state object (or use `{ $key: key }`
36
- * when `data` is not defined).
37
- * 2. **Mix in methods** — each entry in `service.methods` is copied onto the
38
- * instance as a regular function bound to `this`.
39
- * 3. **`setup()`** — called once on the new instance; may be async.
40
- *
41
- * @param service - The service definition.
42
- * @param key - The scope key (`'singleton'`, `null`, or a session ID).
43
- * @returns A fully initialised service instance.
44
- */
45
- function buildModule(service, key) {
46
- const instance = service.data
47
- ? service.data(key)
48
- : { $key: key };
49
- // Mix service methods into the instance, bound to `this = instance`.
50
- // BUG FIX: the original used arrow functions `() => method.apply(module, arguments)`.
51
- // Arrow functions do NOT have their own `arguments` object — they inherit it
52
- // from the enclosing `buildModule` scope (which holds `(service, key)`).
53
- // Any arguments forwarded to the method were therefore silently dropped.
54
- // Corrected to a regular function expression that captures its own `arguments`
55
- // via a rest parameter spread.
56
- if (service.methods) {
57
- for (const methodName of Object.keys(service.methods)) {
58
- const method = service.methods[methodName];
59
- instance[methodName] = function (...args) {
60
- return method.apply(instance, args);
61
- };
62
- }
63
- }
64
- if (service.setup)
65
- service.setup.apply(instance, []);
66
- return instance;
67
- }
68
- /**
69
- * Resolve the correct service instance for an incoming request.
70
- *
71
- * Instance lifecycle by scope:
72
- * - **Singleton** (`scope` absent): always returns `modules['singleton']`.
73
- * - **Keyed** (`scope` returns a string): look up `modules[key]`; create and
74
- * cache a new instance on first access.
75
- * - **Ephemeral** (`scope` returns `null`): create a fresh instance every time;
76
- * never cached.
77
- *
78
- * @param service - The service definition.
79
- * @param modules - The instance cache (mutated when a new keyed instance is created).
80
- * @param req - The incoming request.
81
- * @returns The service instance to use for this request.
82
- */
83
- function resolveInstance(service, modules, req) {
84
- if (typeof service.scope !== 'function') {
85
- // Singleton — always the same global instance.
86
- return modules['singleton'];
87
- }
88
- // BUG FIX: the original called `service.key(req)` which does not exist on
89
- // the service definition. The correct method is `service.scope(req)`.
90
- const key = service.scope(req);
91
- if (key === null) {
92
- // Ephemeral — create a fresh, uncached instance for every request.
93
- return buildModule(service, null);
94
- }
95
- // Keyed — retrieve from cache or create and store.
96
- if (!modules[key])
97
- modules[key] = buildModule(service, key);
98
- return modules[key];
99
- }
100
- // ---------------------------------------------------------------------------
101
- // Response helpers
102
- // ---------------------------------------------------------------------------
103
- /**
104
- * Send a JSON response with the appropriate `Content-Type` header.
105
- *
106
- * @param res - The outgoing response.
107
- * @param data - Any JSON-serialisable value.
108
- */
109
- function sendJson(res, data) {
110
- // BUG FIX: the original called `res.send(JSON.stringify(val))` without
111
- // setting a `Content-Type` header. Clients had no way to detect that the
112
- // response body was JSON. Corrected by setting the header explicitly.
113
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
114
- res.send(JSON.stringify(data));
115
- }
116
- /**
117
- * Translate a caught error (thrown or rejected by a service method) into an
118
- * HTTP error response.
119
- *
120
- * Expected shape: `{ httpStatus?, data?, message? }` (see {@link ApiError}).
121
- * Any other thrown value is treated as an opaque 500 Internal Server Error.
122
- *
123
- * @param res - The outgoing response.
124
- * @param err - The caught value.
125
- */
126
- function sendError(res, err) {
127
- const e = err;
128
- const status = e?.httpStatus ?? 500;
129
- if (e?.data !== undefined) {
130
- res.setHeader('Content-Type', 'application/json; charset=utf-8');
131
- res.status(status).send(JSON.stringify(e.data));
132
- }
133
- else {
134
- res.status(status).send(e?.message ?? 'Internal error');
135
- }
136
- }
137
- // ---------------------------------------------------------------------------
138
- // Public API
139
- // ---------------------------------------------------------------------------
140
- /**
141
- * Build an Express-compatible router from a service definition object.
142
- *
143
- * The returned router is suitable for mounting via `app.use()`:
144
- *
145
- * ```ts
146
- * import myService from './my-service.js';
147
- *
148
- * app.use('/api', apiBuilder(myService));
149
- * ```
150
- *
151
- * **Route handlers** declared in `service.GET`, `service.POST`, etc. are
152
- * called with `this` bound to the service instance. They receive two
153
- * arguments:
154
- * 1. `params` — merged route + query-string parameters from `req.params`.
155
- * 2. `body` — the parsed request body from `req.body` (requires a
156
- * body-parsing middleware such as `json()` to run first).
157
- *
158
- * **Return values:**
159
- * - A **truthy value** (or a `Promise` resolving to one) → serialised as JSON
160
- * with status `200 OK`.
161
- * - A **falsy value** (`undefined`, `null`, `false`, `0`, `''`) or a
162
- * `Promise` resolving to one → `201 No Content` (useful for mutations).
163
- *
164
- * **Error handling:**
165
- * - Throwing or rejecting with `{ httpStatus, message }` sends the
166
- * corresponding HTTP error.
167
- * - Throwing or rejecting with `{ httpStatus, data }` sends the `data` object
168
- * as a JSON body.
169
- * - Any other thrown value produces `500 Internal Server Error`.
170
- *
171
- * @param service - The service definition (see {@link ServiceDefinition}).
172
- * @returns A router instance pre-configured with all declared routes.
173
- */
174
- function apiBuilder(service) {
175
- const api = (0, router_js_1.default)();
176
- const modules = {};
177
- // Pre-build the singleton instance eagerly so `setup()` runs at startup.
178
- if (typeof service.scope !== 'function')
179
- // BUG FIX: the original called `buildModule(service)` without a key,
180
- // passing `undefined` to `data()` and leaving `$key` as `undefined`.
181
- // Corrected to pass `'singleton'` as the canonical key.
182
- modules['singleton'] = buildModule(service, 'singleton');
183
- /**
184
- * Register all route handlers from a route map for a given HTTP method.
185
- *
186
- * Each handler:
187
- * 1. Resolves the correct service instance (singleton / keyed / ephemeral).
188
- * 2. Invokes the service method with `(params, body)`.
189
- * 3. Sends the return value as JSON (or 201 if falsy).
190
- * 4. Catches thrown / rejected {@link ApiError} objects and translates them
191
- * into the appropriate HTTP error response.
192
- *
193
- * @param routeMap - Map of path patterns to service methods (`undefined` = skip).
194
- * @param register - Registers a handler on the router for the current HTTP method.
195
- */
196
- function buildRoutes(routeMap, register) {
197
- if (!routeMap)
198
- return;
199
- // Sort routes by decreasing specificity so that more precise patterns
200
- // (more segments, fewer parameters) are registered first in the router.
201
- // Without this, a plain path like '/items' would match '/items/1' as a
202
- // prefix and steal requests intended for '/items/:id'.
203
- // Specificity = (segment count * 100) - (parameter count * 10).
204
- const sortedPaths = Object.keys(routeMap).sort((a, b) => {
205
- const score = (p) => {
206
- const segs = p.split('/').filter(s => s.length > 0);
207
- return segs.length * 100 - segs.filter(s => s.startsWith(':')).length * 10;
208
- };
209
- return score(b) - score(a) || b.localeCompare(a);
210
- });
211
- for (const path of sortedPaths) {
212
- const method = routeMap[path];
213
- register(path, (req, res) => {
214
- const params = req.params;
215
- const body = req.body;
216
- try {
217
- const instance = resolveInstance(service, modules, req);
218
- const ret = method.apply(instance, [params, body]);
219
- if (ret instanceof Promise) {
220
- ret
221
- .then((val) => {
222
- if (val !== undefined && val !== null && val !== false && val !== 0 && val !== '')
223
- sendJson(res, val);
224
- else
225
- res.status(201).end();
226
- })
227
- .catch((err) => sendError(res, err));
228
- }
229
- else {
230
- if (ret !== undefined && ret !== null && ret !== false && ret !== 0 && ret !== '')
231
- sendJson(res, ret);
232
- else
233
- res.status(201).end();
234
- }
235
- }
236
- catch (err) {
237
- sendError(res, err);
238
- }
239
- });
240
- }
241
- }
242
- buildRoutes(service.GET, (path, h) => api.get(path, h));
243
- buildRoutes(service.POST, (path, h) => api.post(path, h));
244
- buildRoutes(service.PUT, (path, h) => api.put(path, h));
245
- buildRoutes(service.DELETE, (path, h) => api.delete(path, h));
246
- buildRoutes(service.PATCH, (path, h) => api.patch(path, h));
247
- return api;
248
- }
249
- exports.default = apiBuilder;
250
- //# sourceMappingURL=apis.js.map
package/dist/git.js DELETED
@@ -1,244 +0,0 @@
1
- /* Copyright 2021 Fabien Bavent
2
- *
3
- * Permission is hereby granted, free of charge, to any person obtaining a
4
- * copy of this software and associated documentation files (the "Software"),
5
- * to deal in the Software without restriction, including without limitation
6
- * the rights to use, copy, modify, merge, publish, distribute, sublicense,
7
- * and/or sell copies of the Software, and to permit persons to whom the
8
- * Software is furnished to do so, subject to the following conditions:
9
- *
10
- * The above copyright notice and this permission notice shall be included
11
- * in all copies or substantial portions of the Software.
12
- *
13
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
14
- * OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
- * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
16
- * THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
- * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
18
- * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
19
- * DEALINGS IN THE SOFTWARE.
20
- */
21
- 'use strict';
22
- Object.defineProperty(exports, "__esModule", { value: true });
23
- exports.gitHandler = gitHandler;
24
- const child_process_1 = require("child_process");
25
- const zlib_1 = require("zlib");
26
- // ---------------------------------------------------------------------------
27
- // PKT-LINE helpers
28
- // ---------------------------------------------------------------------------
29
- /**
30
- * Encode a string as a Git PKT-LINE frame.
31
- *
32
- * The Git Smart HTTP protocol wraps each line in a 4-hex-digit length prefix
33
- * that counts the total frame length (4 bytes for the prefix itself plus the
34
- * payload bytes, **not** characters).
35
- *
36
- * @param str - The plain-text payload to wrap (must be ASCII or UTF-8).
37
- * @returns The framed string, e.g. `"001e# service=git-upload-pack\n"`.
38
- *
39
- * @see https://git-scm.com/docs/pack-protocol#_pkt_line_format
40
- *
41
- * @example
42
- * ```ts
43
- * pktLine('# service=git-upload-pack\n')
44
- * // → '001e# service=git-upload-pack\n'
45
- * // ^^^^ 4 + 26 = 30 = 0x1e
46
- * ```
47
- */
48
- function pktLine(str) {
49
- // BUG FIX: the original used `str.length` (character count / UTF-16 code
50
- // units) instead of the actual byte length. The Git PKT-LINE spec requires
51
- // the 4-hex-digit prefix to represent the *byte* count of the whole frame
52
- // (prefix + payload). For ASCII-only service names this difference is zero,
53
- // but using Buffer.byteLength is correct and future-proof.
54
- const byteLen = Buffer.byteLength(str, 'utf8') + 4; // +4 for the 4-char hex prefix itself
55
- return byteLen.toString(16).padStart(4, '0') + str;
56
- }
57
- /**
58
- * The Git PKT-LINE flush packet.
59
- * Signals the end of a list of PKT-LINE records.
60
- */
61
- const PKT_FLUSH = '0000';
62
- // ---------------------------------------------------------------------------
63
- // Middleware factory
64
- // ---------------------------------------------------------------------------
65
- /**
66
- * Middleware factory that exposes a Git repository over the **Git Smart HTTP
67
- * protocol** (read-only fetch / clone, via `git-upload-pack`).
68
- *
69
- * Mount this handler at the root of a repository-scoped path so that the two
70
- * sub-paths it handles (`/info/refs` and `/git-upload-pack`) are reachable:
71
- *
72
- * ```ts
73
- * app.use('/repos/:repo', gitHandler({
74
- * repository: (req) => path.join('/srv/git', req.params.repo + '.git'),
75
- * }));
76
- * ```
77
- *
78
- * **Supported endpoints:**
79
- *
80
- * | Method | Path | Purpose |
81
- * |--------|-------------------|----------------------------------------------|
82
- * | GET | `/info/refs` | Smart HTTP capability advertisement |
83
- * | POST | `/git-upload-pack`| Pack-file negotiation and transfer |
84
- *
85
- * Only `git-upload-pack` (fetch / clone) is implemented.
86
- * `git-receive-pack` (push) is intentionally excluded.
87
- *
88
- * **Compression:** gzip-compressed POST bodies are transparently decompressed
89
- * before being piped to `git-upload-pack`.
90
- *
91
- * @param opt - Handler configuration (see {@link GitHandlerOptions}).
92
- * @returns An Express-compatible middleware function `(req, res) => void`.
93
- * @throws {TypeError} When `opt.repository` is not a function.
94
- */
95
- function gitHandler(opt) {
96
- if (typeof opt.repository !== 'function')
97
- throw new TypeError('gitHandler: opt.repository must be a function');
98
- const gitBin = (opt.gitPath ?? '') + 'git-upload-pack';
99
- return (req, res) => {
100
- // Resolve the repository path for this request.
101
- const gitDirectory = opt.repository(req);
102
- if (!gitDirectory)
103
- return void res.status(404).send('Repository not found');
104
- const urlPath = req.path; // sub-path after the mount prefix
105
- // ── GET /info/refs?service=git-upload-pack ──────────────────────────
106
- if (req.method === 'GET' && urlPath === '/info/refs') {
107
- // BUG FIX: `req.queries.url` can be undefined when no query parameters
108
- // are present, causing `req.queries.url.service` to throw a TypeError.
109
- const service = req.queries?.url?.service;
110
- if (service !== 'git-upload-pack')
111
- return void res.status(403).send('Only git-upload-pack is supported');
112
- res.setHeader('Content-Type', `application/x-${service}-advertisement`);
113
- res.setHeader('Cache-Control', 'no-cache');
114
- // The Smart HTTP advertisement starts with a PKT-LINE service banner
115
- // followed by a flush packet (0000), then the git-upload-pack output.
116
- res.write(pktLine(`# service=${service}\n`));
117
- res.write(PKT_FLUSH);
118
- const args = buildArgs(opt, ['--stateless-rpc', '--advertise-refs', gitDirectory]);
119
- const proc = (0, child_process_1.spawn)(gitBin, args, {
120
- env: { ...process.env, GIT_PROTOCOL: req.headers['git-protocol'] || '' },
121
- });
122
- // BUG FIX: the original did not listen for spawn errors (e.g. ENOENT
123
- // when git is not installed). Without this handler, a missing binary
124
- // causes an uncaught exception that crashes the server process.
125
- proc.on('error', (err) => {
126
- console.error('[git-upload-pack refs] spawn error:', err.message);
127
- if (!res.writableEnded)
128
- res.status(500).send(`git-upload-pack unavailable: ${err.message}`);
129
- });
130
- proc.stdout.pipe(res);
131
- proc.stdout.on('error', (err) => {
132
- console.warn('[git-upload-pack refs] stdout error:', err.message);
133
- });
134
- proc.stderr.on('data', (d) => console.error('[git-upload-pack refs]', d.toString()));
135
- proc.on('close', (code) => {
136
- if (code !== 0) {
137
- console.error(`[git-upload-pack refs] exited with code ${code}`);
138
- if (!res.writableEnded)
139
- res.status(500).send('git-upload-pack failed');
140
- }
141
- // When code === 0, proc.stdout has already piped all data and called
142
- // res.end() automatically (default pipe behaviour).
143
- });
144
- return;
145
- }
146
- // ── POST /git-upload-pack ───────────────────────────────────────────
147
- if (req.method === 'POST' && urlPath === '/git-upload-pack') {
148
- const contentType = req.headers['content-type'] || '';
149
- // BUG FIX: the original called `res.send(415).send(...)`. Our router's
150
- // `res.send()` signature is `send(body?)` — passing 415 as the body
151
- // writes the number as a string, then the chained `.send()` fails
152
- // because `res.send()` already ended the response. Corrected to
153
- // `res.status(415).send(...)`.
154
- if (contentType !== 'application/x-git-upload-pack-request')
155
- return void res.status(415).send('Unsupported Media Type');
156
- res.setHeader('Content-Type', 'application/x-git-upload-pack-result');
157
- res.setHeader('Cache-Control', 'no-cache');
158
- const args = buildArgs(opt, ['--stateless-rpc', gitDirectory]);
159
- const proc = (0, child_process_1.spawn)(gitBin, args, {
160
- env: { ...process.env, GIT_PROTOCOL: req.headers['git-protocol'] || '' },
161
- });
162
- // BUG FIX: same missing spawn-error handler as the GET branch.
163
- proc.on('error', (err) => {
164
- console.error('[git-upload-pack pack] spawn error:', err.message);
165
- if (!res.writableEnded)
166
- res.status(500).send(`git-upload-pack unavailable: ${err.message}`);
167
- });
168
- // Transparently decompress gzip-encoded request bodies.
169
- const encoding = req.headers['content-encoding'];
170
- if (encoding === 'gzip') {
171
- const gunzip = (0, zlib_1.createGunzip)();
172
- gunzip.on('error', (err) => {
173
- console.warn('[git-upload-pack pack] gunzip error:', err.message);
174
- if (!res.writableEnded)
175
- res.status(400).send('Failed to decompress request body');
176
- });
177
- req.pipe(gunzip).pipe(proc.stdin);
178
- }
179
- else {
180
- req.pipe(proc.stdin);
181
- }
182
- proc.stdout.pipe(res);
183
- proc.stdout.on('error', (err) => {
184
- console.warn('[git-upload-pack pack] stdout error:', err.message);
185
- });
186
- // BUG FIX: the original tagged the POST stderr with the same label as
187
- // the GET branch ('[git-upload-pack refs]'), making log messages from
188
- // the two branches indistinguishable. Corrected to '[git-upload-pack pack]'.
189
- proc.stderr.on('data', (d) => console.error('[git-upload-pack pack]', d.toString()));
190
- proc.on('close', (code) => {
191
- if (code !== 0) {
192
- console.error(`[git-upload-pack pack] exited with code ${code}`);
193
- if (!res.writableEnded)
194
- res.status(500).send('git-upload-pack failed');
195
- }
196
- });
197
- proc.stdin.on('error', (err) => {
198
- // EPIPE is expected when the client disconnects mid-stream; it is not
199
- // a server-side fault and does not require an error response.
200
- if (err.code !== 'EPIPE')
201
- console.warn('[git-upload-pack pack] stdin error:', err.message);
202
- });
203
- return;
204
- }
205
- // Unrecognised path inside the repository mount.
206
- res.status(404).send('Not found');
207
- };
208
- }
209
- // ---------------------------------------------------------------------------
210
- // Internal helpers
211
- // ---------------------------------------------------------------------------
212
- /**
213
- * Build the argument list for `git-upload-pack`.
214
- *
215
- * Prepends an optional `--timeout=<n>` argument when configured, then
216
- * inserts the `--strict` / `--no-strict` flag, followed by any additional
217
- * arguments the caller provides.
218
- *
219
- * @param opt - Handler options.
220
- * @param trailing - Arguments appended after the strict flag
221
- * (e.g. `['--stateless-rpc', '--advertise-refs', dir]`).
222
- * @returns The complete argument array ready for `spawn`.
223
- */
224
- function buildArgs(opt, trailing) {
225
- const args = [];
226
- if (opt.timeout) {
227
- // BUG FIX: `parseInt` without an explicit radix may misinterpret strings
228
- // that start with '0' as octal in some environments. Always pass base 10.
229
- const seconds = parseInt(String(opt.timeout), 10);
230
- if (!isNaN(seconds) && seconds > 0)
231
- args.push(`--timeout=${seconds}`);
232
- }
233
- // BUG FIX: the original used `opt.bareOnly ? '--strict' : '--no-strict'`.
234
- // `git-upload-pack` does not accept `--strict` — that flag belongs to
235
- // `git-receive-pack`. The correct option for upload-pack is `--no-strict`
236
- // (which relaxes the requirement that the path must be a bare repository).
237
- // When the caller sets `strict: true` we simply omit `--no-strict`; when
238
- // `strict` is false (default) we pass `--no-strict` to allow non-bare repos.
239
- if (!opt.strict)
240
- args.push('--no-strict');
241
- return [...args, ...trailing];
242
- }
243
- exports.default = gitHandler;
244
- //# sourceMappingURL=git.js.map