@blamejs/core 0.10.15 → 0.11.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.
@@ -0,0 +1,90 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.importmapIntegrity
4
+ * @nav HTTP
5
+ * @title Import-Map Integrity
6
+ * @order 175
7
+ *
8
+ * @intro
9
+ * WICG Import Maps + Subresource Integrity (SRI) extension. When
10
+ * a page declares `<script type="importmap">`, each mapped module
11
+ * SHOULD carry an `integrity` hash so the browser refuses to
12
+ * execute the module if the bytes don't match.
13
+ *
14
+ * `b.importmapIntegrity.build({ specifiers, sha256, sha384,
15
+ * sha512 })` hashes each operator-supplied module body and emits
16
+ * the `<script type="importmap">` JSON with an `integrity` map
17
+ * alongside the `imports` map. Composes existing `b.crypto.sri`.
18
+ *
19
+ * @card
20
+ * WICG Import Maps + SRI integrity builder. Emits importmap JSON with module-body SHA-384 hashes so browsers refuse unauthenticated module bytes.
21
+ */
22
+
23
+ var validateOpts = require("./validate-opts");
24
+ var bCrypto = require("./crypto");
25
+ var { defineClass } = require("./framework-error");
26
+
27
+ var ImportmapError = defineClass("ImportmapError", { alwaysPermanent: true });
28
+
29
+ /**
30
+ * @primitive b.importmapIntegrity.build
31
+ * @signature b.importmapIntegrity.build(opts)
32
+ * @since 0.10.16
33
+ * @status stable
34
+ *
35
+ * Build an import-map JSON shape `{ imports, integrity }` per WICG
36
+ * Import-Maps-SRI. Each module body is hashed with `opts.hash`
37
+ * (default sha384 per current SRI convention).
38
+ *
39
+ * @opts
40
+ * modules: { "<specifier>": { url, body: Buffer|string } },
41
+ * hash: "sha256"|"sha384"|"sha512", // default sha384
42
+ *
43
+ * @example
44
+ * var im = b.importmapIntegrity.build({
45
+ * modules: {
46
+ * "@org/lib": { url: "/static/lib.js", body: fileBytes },
47
+ * },
48
+ * });
49
+ * res.end("<script type=\"importmap\">" + JSON.stringify(im) + "</script>");
50
+ */
51
+ function build(opts) {
52
+ opts = validateOpts.requireObject(opts, "importmapIntegrity.build",
53
+ ImportmapError, "importmap/bad-opts");
54
+ validateOpts(opts, ["modules", "hash"], "importmapIntegrity.build");
55
+ if (!opts.modules || typeof opts.modules !== "object" || Array.isArray(opts.modules)) {
56
+ throw new ImportmapError("importmap/no-modules",
57
+ "build: opts.modules must be a non-array object");
58
+ }
59
+ var hash = opts.hash || "sha384";
60
+ if (hash !== "sha256" && hash !== "sha384" && hash !== "sha512") {
61
+ throw new ImportmapError("importmap/bad-hash",
62
+ "build: hash must be sha256 / sha384 / sha512");
63
+ }
64
+ var imports = {};
65
+ var integrity = {};
66
+ var keys = Object.keys(opts.modules);
67
+ for (var i = 0; i < keys.length; i += 1) {
68
+ var spec = keys[i];
69
+ var mod = opts.modules[spec];
70
+ if (!mod || typeof mod.url !== "string") {
71
+ throw new ImportmapError("importmap/bad-module",
72
+ "build: modules['" + spec + "'].url must be a string");
73
+ }
74
+ if (!Buffer.isBuffer(mod.body) && typeof mod.body !== "string") {
75
+ throw new ImportmapError("importmap/bad-module",
76
+ "build: modules['" + spec + "'].body must be a Buffer or string");
77
+ }
78
+ imports[spec] = mod.url;
79
+ // Compose b.crypto.sri — returns the canonical SRI string
80
+ // (e.g. `sha384-<base64>`). b.crypto.sri takes its hash from
81
+ // `opts.algorithm`, not a positional arg.
82
+ integrity[mod.url] = bCrypto.sri(mod.body, { algorithm: hash });
83
+ }
84
+ return { imports: imports, integrity: integrity };
85
+ }
86
+
87
+ module.exports = {
88
+ build: build,
89
+ ImportmapError: ImportmapError,
90
+ };
package/lib/jsonapi.js ADDED
@@ -0,0 +1,230 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.jsonApi
4
+ * @nav HTTP
5
+ * @title JSON:API
6
+ * @order 172
7
+ *
8
+ * @intro
9
+ * JSON:API v1.1 (jsonapi.org/format/1.1/) response-shape helpers.
10
+ * The framework's wire-format primitives compose this so operators
11
+ * building JSON:API services get the right top-level shape + the
12
+ * right Content-Type without re-implementing the spec each time.
13
+ *
14
+ * Content-Type: `application/vnd.api+json`
15
+ *
16
+ * Top-level shapes:
17
+ * - `dataResponse(data, opts?)` — `{ data: [...] | {...}, included?, links?, meta? }`
18
+ * - `errorResponse(errors)` — `{ errors: [...] }`
19
+ * - `linkObject(url, opts?)` — string href OR `{ href, rel, meta }`
20
+ *
21
+ * @card
22
+ * JSON:API v1.1 response shape builders. Content-Type negotiation, top-level data/errors/included/links/meta wrappers, error-object shape per §7.
23
+ */
24
+
25
+ var validateOpts = require("./validate-opts");
26
+ var numericBounds = require("./numeric-bounds");
27
+ var { defineClass } = require("./framework-error");
28
+
29
+ var JsonApiError = defineClass("JsonApiError", { alwaysPermanent: true });
30
+
31
+ var CONTENT_TYPE = "application/vnd.api+json";
32
+
33
+ /**
34
+ * @primitive b.jsonApi.dataResponse
35
+ * @signature b.jsonApi.dataResponse(data, opts?)
36
+ * @since 0.10.16
37
+ * @status stable
38
+ *
39
+ * Build a JSON:API v1.1 success response. `data` can be a Resource
40
+ * Object, an array of Resource Objects, or null (for single-resource
41
+ * 404 / empty-collection responses). Each Resource Object must carry
42
+ * `type` + `id` (§7.2).
43
+ *
44
+ * @opts
45
+ * included: ResourceObject[], // compound documents §7.7
46
+ * links: object, // top-level links §7.5
47
+ * meta: object, // non-standard top-level meta §7.4
48
+ * jsonapi: object, // jsonapi-object §7.3 (version etc.)
49
+ *
50
+ * @example
51
+ * res.setHeader("Content-Type", "application/vnd.api+json");
52
+ * res.end(JSON.stringify(b.jsonApi.dataResponse(
53
+ * { type: "articles", id: "1", attributes: { title: "Hello" } },
54
+ * { links: { self: "/articles/1" } }
55
+ * )));
56
+ */
57
+ function dataResponse(data, opts) {
58
+ opts = opts || {};
59
+ validateOpts(opts, ["included", "links", "meta", "jsonapi"], "jsonApi.dataResponse");
60
+ if (data !== null && data !== undefined) {
61
+ if (Array.isArray(data)) {
62
+ for (var i = 0; i < data.length; i += 1) _assertResource(data[i], i);
63
+ } else {
64
+ _assertResource(data, null);
65
+ }
66
+ }
67
+ var out = { data: data === undefined ? null : data };
68
+ if (opts.included) {
69
+ if (!Array.isArray(opts.included)) {
70
+ throw new JsonApiError("json-api/bad-included",
71
+ "dataResponse: opts.included must be an array");
72
+ }
73
+ for (var j = 0; j < opts.included.length; j += 1) _assertResource(opts.included[j], j);
74
+ out.included = opts.included;
75
+ }
76
+ if (opts.links) out.links = opts.links;
77
+ if (opts.meta) out.meta = opts.meta;
78
+ if (opts.jsonapi) out.jsonapi = opts.jsonapi;
79
+ return out;
80
+ }
81
+
82
+ /**
83
+ * @primitive b.jsonApi.errorResponse
84
+ * @signature b.jsonApi.errorResponse(errors, opts?)
85
+ * @since 0.10.16
86
+ * @status stable
87
+ *
88
+ * Build a JSON:API v1.1 error response per §7.6. Each error object
89
+ * can carry `id` / `status` / `code` / `title` / `detail` / `source` /
90
+ * `links` / `meta`. The framework refuses errors lacking BOTH
91
+ * `status` and `title` (most JSON:API consumers need at least one).
92
+ *
93
+ * @opts
94
+ * meta: object, // optional top-level `meta` block (JSON:API §5.3)
95
+ * jsonapi: object, // optional top-level `jsonapi` member (JSON:API §5.2 — version / ext / profile)
96
+ * links: object, // optional top-level `links` (JSON:API §5.4 — self / related / pagination)
97
+ *
98
+ * @example
99
+ * res.statusCode = 422;
100
+ * res.end(JSON.stringify(b.jsonApi.errorResponse([
101
+ * { status: "422", code: "INVALID", title: "Invalid email",
102
+ * source: { pointer: "/data/attributes/email" } },
103
+ * ])));
104
+ */
105
+ function errorResponse(errors, opts) {
106
+ opts = opts || {};
107
+ if (!Array.isArray(errors) || errors.length === 0) {
108
+ throw new JsonApiError("json-api/no-errors",
109
+ "errorResponse: errors must be a non-empty array");
110
+ }
111
+ var checked = errors.map(function (e, idx) {
112
+ if (!e || typeof e !== "object") {
113
+ throw new JsonApiError("json-api/bad-error",
114
+ "errorResponse: errors[" + idx + "] must be an object");
115
+ }
116
+ if (typeof e.status !== "string" && typeof e.title !== "string") {
117
+ throw new JsonApiError("json-api/empty-error",
118
+ "errorResponse: errors[" + idx + "] must have at least 'status' or 'title' (string)");
119
+ }
120
+ return e;
121
+ });
122
+ var out = { errors: checked };
123
+ if (opts.meta) out.meta = opts.meta;
124
+ if (opts.jsonapi) out.jsonapi = opts.jsonapi;
125
+ if (opts.links) out.links = opts.links;
126
+ return out;
127
+ }
128
+
129
+ function _assertResource(r, idx) {
130
+ if (!r || typeof r !== "object") {
131
+ throw new JsonApiError("json-api/bad-resource",
132
+ "Resource at " + (idx === null ? "<root>" : "index " + idx) + " must be an object");
133
+ }
134
+ if (typeof r.type !== "string" || r.type.length === 0) {
135
+ throw new JsonApiError("json-api/missing-type",
136
+ "Resource at " + (idx === null ? "<root>" : "index " + idx) + " missing 'type'");
137
+ }
138
+ // id is OPTIONAL only on client-side create requests; we don't have
139
+ // a way to distinguish, so we accept missing id (the operator's
140
+ // responsibility to set it for non-create paths).
141
+ }
142
+
143
+ /**
144
+ * @primitive b.jsonApi.parseQuery
145
+ * @signature b.jsonApi.parseQuery(queryString, opts?)
146
+ * @since 0.10.16
147
+ * @status stable
148
+ *
149
+ * Parse a JSON:API v1.1 query string per §5 (Fetching Data). Returns
150
+ * `{ include, fields, filter, sort, page }`:
151
+ * - `include` — array of relationship paths from `include=` (comma-split)
152
+ * - `fields[type]` — array of sparse-fieldset selectors per type
153
+ * - `filter` — pass-through object (spec defers filter shape to operators)
154
+ * - `sort` — array of `{ field, asc }` per RFC 7159-style direction
155
+ * - `page` — pass-through object (operator picks page-strategy)
156
+ *
157
+ * Refuses missing required `include` paths when opts.includeAllowlist is
158
+ * supplied and an unrecognized path appears.
159
+ *
160
+ * @opts
161
+ * includeAllowlist: string[],
162
+ * sortAllowlist: string[],
163
+ * maxIncludeDepth: number, // default 5
164
+ *
165
+ * @example
166
+ * var q = b.jsonApi.parseQuery(req.url.split("?")[1]);
167
+ * q.include; // → ["author", "comments.author"]
168
+ * q.fields.articles; // → ["title", "body"]
169
+ * q.sort; // → [{ field: "createdAt", asc: false }]
170
+ */
171
+ function parseQuery(queryString, opts) {
172
+ opts = opts || {};
173
+ if (typeof queryString !== "string") {
174
+ throw new JsonApiError("json-api/bad-query",
175
+ "parseQuery: queryString must be a string");
176
+ }
177
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxIncludeDepth, "maxIncludeDepth",
178
+ JsonApiError, "json-api/bad-max-include-depth");
179
+ var maxDepth = typeof opts.maxIncludeDepth === "number" ? opts.maxIncludeDepth : 5;
180
+ var out = { include: [], fields: {}, filter: {}, sort: [], page: {} };
181
+ if (queryString.length === 0) return out;
182
+ var pairs = queryString.split("&");
183
+ for (var i = 0; i < pairs.length; i += 1) {
184
+ var eq = pairs[i].indexOf("=");
185
+ if (eq === -1) continue;
186
+ var rawKey = decodeURIComponent(pairs[i].slice(0, eq));
187
+ var rawVal = decodeURIComponent(pairs[i].slice(eq + 1));
188
+ if (rawKey === "include") {
189
+ out.include = rawVal.split(",").map(function (s) { return s.trim(); }).filter(Boolean);
190
+ for (var ii = 0; ii < out.include.length; ii += 1) {
191
+ var depth = out.include[ii].split(".").length;
192
+ if (depth > maxDepth) {
193
+ throw new JsonApiError("json-api/include-too-deep",
194
+ "parseQuery: include path '" + out.include[ii] + "' exceeds maxIncludeDepth=" + maxDepth);
195
+ }
196
+ if (Array.isArray(opts.includeAllowlist) &&
197
+ opts.includeAllowlist.indexOf(out.include[ii]) === -1) {
198
+ throw new JsonApiError("json-api/include-not-allowed",
199
+ "parseQuery: include path '" + out.include[ii] + "' not in allowlist");
200
+ }
201
+ }
202
+ } else if (rawKey === "sort") {
203
+ out.sort = rawVal.split(",").map(function (s) { return s.trim(); }).filter(Boolean).map(function (s) {
204
+ var asc = true;
205
+ if (s.charAt(0) === "-") { asc = false; s = s.slice(1); }
206
+ if (Array.isArray(opts.sortAllowlist) && opts.sortAllowlist.indexOf(s) === -1) {
207
+ throw new JsonApiError("json-api/sort-not-allowed",
208
+ "parseQuery: sort field '" + s + "' not in allowlist");
209
+ }
210
+ return { field: s, asc: asc };
211
+ });
212
+ } else if (rawKey.indexOf("fields[") === 0 && rawKey.charAt(rawKey.length - 1) === "]") {
213
+ var type = rawKey.slice(7, -1); // allow:raw-byte-literal — `fields[` length
214
+ out.fields[type] = rawVal.split(",").map(function (s) { return s.trim(); }).filter(Boolean);
215
+ } else if (rawKey.indexOf("filter[") === 0 && rawKey.charAt(rawKey.length - 1) === "]") {
216
+ out.filter[rawKey.slice(7, -1)] = rawVal; // allow:raw-byte-literal — `filter[` length
217
+ } else if (rawKey.indexOf("page[") === 0 && rawKey.charAt(rawKey.length - 1) === "]") {
218
+ out.page[rawKey.slice(5, -1)] = rawVal; // allow:raw-byte-literal — `page[` length
219
+ }
220
+ }
221
+ return out;
222
+ }
223
+
224
+ module.exports = {
225
+ dataResponse: dataResponse,
226
+ errorResponse: errorResponse,
227
+ parseQuery: parseQuery,
228
+ CONTENT_TYPE: CONTENT_TYPE,
229
+ JsonApiError: JsonApiError,
230
+ };
package/lib/lro.js ADDED
@@ -0,0 +1,200 @@
1
+ "use strict";
2
+ /**
3
+ * @module b.lro
4
+ * @nav HTTP
5
+ * @title Long-Running Operations
6
+ * @order 178
7
+ *
8
+ * @intro
9
+ * Google API Improvement Proposals AIP-151 (Long-Running Operations)
10
+ * — a uniform shape for async APIs where the response can't be
11
+ * computed within the request lifetime. The operator's POST
12
+ * endpoint returns an Operation resource (`name` / `done` /
13
+ * `metadata` / `response` | `error`) which the client polls at
14
+ * `/operations/<name>` until `done: true`. Composes existing
15
+ * `b.queue` / `b.jobs` for the actual work; this module just
16
+ * ships the wire-shape + status-poll endpoint helpers.
17
+ *
18
+ * `b.lro.create(opts)` returns `{ submit, status, list, cancel }`
19
+ * wired to operator-supplied storage (in-memory by default for
20
+ * single-process; ops with multiple workers wire `b.cache` /
21
+ * `b.db` storage via `opts.store`).
22
+ *
23
+ * @card
24
+ * AIP-151 Long-Running Operations — uniform Operation resource shape + submit / status / cancel endpoints. Composes b.queue / b.jobs for the actual work.
25
+ */
26
+
27
+ var validateOpts = require("./validate-opts");
28
+ var numericBounds = require("./numeric-bounds");
29
+ var bCrypto = require("./crypto");
30
+ var C = require("./constants");
31
+ var { defineClass } = require("./framework-error");
32
+
33
+ var LroError = defineClass("LroError", { alwaysPermanent: true });
34
+
35
+ /**
36
+ * @primitive b.lro.create
37
+ * @signature b.lro.create(opts?)
38
+ * @since 0.10.16
39
+ * @status stable
40
+ *
41
+ * Create an LRO registry. Returns `{ submit, status, list, cancel }`.
42
+ * Operations are tracked in `opts.store` (a Map-shaped object) or an
43
+ * in-memory Map when omitted. `submit({ work, metadata?, name? })`
44
+ * runs `work` async + returns the initial Operation resource;
45
+ * `status(name)` returns the current Operation (with `done: true`
46
+ * + `response` or `error` set when finished); `cancel(name)`
47
+ * surfaces cancellation back to the work function via the supplied
48
+ * AbortSignal.
49
+ *
50
+ * @opts
51
+ * store: Map-like { get, set, delete, keys },
52
+ * namePrefix: string, // default "operations/"
53
+ * maxConcurrent: number, // soft cap; overflow refuses with lro/too-many
54
+ *
55
+ * @example
56
+ * var lro = b.lro.create();
57
+ * var op = await lro.submit({
58
+ * work: async function (signal) { return await heavyJob(signal); },
59
+ * });
60
+ * res.statusCode = 202;
61
+ * res.end(JSON.stringify(op));
62
+ *
63
+ * // Later, on GET /operations/<name>:
64
+ * res.end(JSON.stringify(lro.status(op.name)));
65
+ */
66
+ function create(opts) {
67
+ opts = opts || {};
68
+ validateOpts(opts, ["store", "namePrefix", "maxConcurrent"],
69
+ "lro.create");
70
+ var store = opts.store || new Map();
71
+ var prefix = opts.namePrefix || "operations/";
72
+ numericBounds.requirePositiveFiniteIntIfPresent(opts.maxConcurrent, "maxConcurrent",
73
+ LroError, "lro/bad-max-concurrent");
74
+ var maxConcurrent = typeof opts.maxConcurrent === "number" ? opts.maxConcurrent : 1024; // allow:raw-byte-literal — default in-flight cap
75
+
76
+ function _newName() { return prefix + bCrypto.generateToken(32); } // allow:raw-byte-literal — 32-char name token
77
+
78
+ function submit(submitOpts) {
79
+ submitOpts = validateOpts.requireObject(submitOpts, "lro.submit",
80
+ LroError, "lro/bad-opts");
81
+ if (typeof submitOpts.work !== "function") {
82
+ throw new LroError("lro/no-work",
83
+ "submit: opts.work must be a function (signal) => Promise<result>");
84
+ }
85
+ var inFlight = 0;
86
+ var keys = store.keys ? Array.from(store.keys()) : [];
87
+ for (var i = 0; i < keys.length; i += 1) {
88
+ var op = store.get(keys[i]);
89
+ if (op && !op.done) inFlight += 1;
90
+ }
91
+ if (inFlight >= maxConcurrent) {
92
+ throw new LroError("lro/too-many",
93
+ "submit: " + inFlight + " operations in flight (cap " + maxConcurrent + ")");
94
+ }
95
+ var name = submitOpts.name || _newName();
96
+ var controller = (typeof AbortController === "function") ? new AbortController() : null;
97
+ var operation = {
98
+ name: name,
99
+ done: false,
100
+ metadata: submitOpts.metadata || {},
101
+ createdAt: new Date().toISOString(),
102
+ };
103
+ store.set(name, operation);
104
+ // Kick off the work async. Operator-supplied function MUST accept
105
+ // an AbortSignal (or ignore it). Errors land on operation.error;
106
+ // successful results land on operation.response per AIP-151 shape.
107
+ Promise.resolve()
108
+ .then(function () { return submitOpts.work(controller ? controller.signal : null); })
109
+ .then(function (response) {
110
+ var stored = store.get(name);
111
+ if (!stored) return;
112
+ // Cancellation precedes resolve — if `cancel()` flipped the
113
+ // operation to CANCELLED first AND the work function ignored
114
+ // the AbortSignal, do NOT overwrite the cancelled terminal
115
+ // state. AIP-151 §6.x: once an operation is `done: true`, it
116
+ // stays done with the first terminal state it landed in.
117
+ if (stored.done) return;
118
+ stored.done = true;
119
+ stored.response = (response === undefined) ? null : response;
120
+ stored.completedAt = new Date().toISOString();
121
+ }, function (err) {
122
+ var stored = store.get(name);
123
+ if (!stored) return;
124
+ // Same guard as the resolve path — once terminal, stay terminal.
125
+ if (stored.done) return;
126
+ stored.done = true;
127
+ // AIP-151 error: { code, message, details? } shape.
128
+ var msg = (err && err.message) || String(err);
129
+ stored.error = { code: 13, message: msg }; // allow:raw-byte-literal — google.rpc.Code.INTERNAL = 13
130
+ if (err && err.code) stored.error.errorCode = err.code;
131
+ stored.completedAt = new Date().toISOString();
132
+ });
133
+ // Store the controller against the operation (off-resource, so
134
+ // serialisation doesn't accidentally export it).
135
+ operation._controller = controller;
136
+ return _stripPrivate(operation);
137
+ }
138
+
139
+ function status(name) {
140
+ if (typeof name !== "string" || name.length === 0) {
141
+ throw new LroError("lro/bad-name", "status: name must be a non-empty string");
142
+ }
143
+ var op = store.get(name);
144
+ if (!op) {
145
+ throw new LroError("lro/not-found", "status: no operation named '" + name + "'");
146
+ }
147
+ return _stripPrivate(op);
148
+ }
149
+
150
+ function list(filter) {
151
+ filter = filter || {};
152
+ var out = [];
153
+ var keys = store.keys ? Array.from(store.keys()) : [];
154
+ for (var i = 0; i < keys.length; i += 1) {
155
+ var op = store.get(keys[i]);
156
+ if (!op) continue;
157
+ if (filter.doneOnly && !op.done) continue;
158
+ if (filter.pendingOnly && op.done) continue;
159
+ out.push(_stripPrivate(op));
160
+ }
161
+ return out;
162
+ }
163
+
164
+ function cancel(name) {
165
+ if (typeof name !== "string" || name.length === 0) {
166
+ throw new LroError("lro/bad-name", "cancel: name must be a non-empty string");
167
+ }
168
+ var op = store.get(name);
169
+ if (!op) {
170
+ throw new LroError("lro/not-found", "cancel: no operation named '" + name + "'");
171
+ }
172
+ if (op.done) return _stripPrivate(op);
173
+ if (op._controller) {
174
+ try { op._controller.abort(); } catch (_e) { /* best-effort */ }
175
+ }
176
+ // Mark cancelled per AIP-151 — error.code 1 = CANCELLED.
177
+ op.done = true;
178
+ op.error = { code: 1, message: "operation cancelled" }; // allow:raw-byte-literal — google.rpc.Code.CANCELLED = 1
179
+ op.completedAt = new Date().toISOString();
180
+ return _stripPrivate(op);
181
+ }
182
+
183
+ void C;
184
+ return { submit: submit, status: status, list: list, cancel: cancel };
185
+ }
186
+
187
+ function _stripPrivate(op) {
188
+ var out = {};
189
+ var keys = Object.keys(op);
190
+ for (var i = 0; i < keys.length; i += 1) {
191
+ if (keys[i].charAt(0) === "_") continue;
192
+ out[keys[i]] = op[keys[i]];
193
+ }
194
+ return out;
195
+ }
196
+
197
+ module.exports = {
198
+ create: create,
199
+ LroError: LroError,
200
+ };