@blamejs/blamejs-shop 0.0.60 → 0.0.62

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,535 @@
1
+ "use strict";
2
+ /**
3
+ * @module shop.codeMinter
4
+ * @title Code minter — bulk-issue single-use discount codes against
5
+ * the coupons primitive.
6
+ *
7
+ * @intro
8
+ * Operators run a campaign and need N unique single-use discount
9
+ * codes (10000 codes for an influencer drop, 50000 for a printed-
10
+ * insert run). The minter generates each code from a confusion-
11
+ * resistant alphabet via `b.crypto.generateBytes`, persists the
12
+ * batch + per-code member rows, and registers every code with the
13
+ * `coupons` primitive in the same call. The coupons surface owns
14
+ * the redemption tier — single-use enforcement, expiry, per-code
15
+ * discount math live there. The minter owns batch identity,
16
+ * collision-safe generation, paginated read-back, and the operator
17
+ * void path.
18
+ *
19
+ * Collision-safe generation. The default alphabet (32 glyphs:
20
+ * `23456789ABCDEFGHJKLMNPQRSTUVWXYZ`) skips `0/1/I/O` so a code
21
+ * read off a printed insert or repeated over the phone doesn't
22
+ * collapse into ambiguous characters. 256 is a multiple of 32, so
23
+ * each random byte modulo-32 lands on a uniform alphabet draw with
24
+ * no modulo-bias correction. For alphabets where `256 % len !== 0`
25
+ * the minter applies rejection sampling — bytes that fall into the
26
+ * non-uniform tail are discarded and re-drawn.
27
+ *
28
+ * Each generated code passes through the per-mint dedupe set + the
29
+ * `coupon_code` UNIQUE constraint at insert time. On a unique-
30
+ * violation (collision against another batch's code, or — vanishing-
31
+ * probability — collision within the same batch's mint loop) the
32
+ * minter retries with a fresh draw, up to a bounded retry budget
33
+ * per code (default 16). If the budget is exhausted the call
34
+ * throws `CODE_MINTER_COLLISION_BUDGET_EXHAUSTED` rather than
35
+ * returning a partial batch — the storage row count and the
36
+ * returned `count_minted` always match.
37
+ *
38
+ * Composition:
39
+ * var cm = bShop.codeMinter.create({ query: q, coupons: cp });
40
+ * var batch = await cm.mintBatch({
41
+ * batch_label: "fall-2026-influencers",
42
+ * count: 10000,
43
+ * length: 10,
44
+ * prefix: "FALL-",
45
+ * coupon_template: {
46
+ * kind: "percent_off",
47
+ * value: 20,
48
+ * expires_at: Date.UTC(2026, 11, 31),
49
+ * },
50
+ * });
51
+ * // batch.batch_id, batch.count_minted, batch.sample_codes (up to 5)
52
+ *
53
+ * Surface:
54
+ * - mintBatch({ batch_label, count, alphabet?, length, prefix?,
55
+ * suffix?, coupon_template })
56
+ * Generates `count` distinct codes and persists each through
57
+ * `coupons.create({ code, ... coupon_template })`. The
58
+ * `coupon_template` payload is opaque to the minter — every
59
+ * key other than `code` is forwarded as-is, so operators
60
+ * choose the discount kind / value / expiry / single-use
61
+ * flag the coupons primitive expects. Returns
62
+ * `{ batch_id, count_minted, sample_codes }`.
63
+ *
64
+ * - getBatch(batch_id) -> batch row or null.
65
+ * - listBatches({ status? }) — ordered created_at DESC, id DESC.
66
+ * - codesForBatch({ batch_id, limit?, cursor? }) — paginated read
67
+ * of member rows. Cursor is the last row's `minted_at` epoch-ms.
68
+ * - voidBatch({ batch_id, reason }) — flips the batch to 'voided'
69
+ * and walks every member row, calling `coupons.archive(code)`
70
+ * on each. Idempotent: voiding a voided batch is a no-op.
71
+ * - exportBatchCsv({ batch_id }) — async-iterable yielding CSV
72
+ * chunks ready for label-printing. The header row is the first
73
+ * yield; each subsequent yield is one member row terminated by
74
+ * `\r\n`. Suitable for streaming to a Response body.
75
+ *
76
+ * Storage:
77
+ * - `code_batches` + `code_batch_members` (migration 0075).
78
+ *
79
+ * @primitive codeMinter
80
+ * @related coupons, b.crypto.generateBytes
81
+ */
82
+
83
+ var bShop;
84
+ function _b() {
85
+ if (!bShop) bShop = require("./index");
86
+ return bShop.framework;
87
+ }
88
+
89
+ // Confusion-resistant default alphabet — 32 glyphs, skips 0/1/I/O.
90
+ // 256 % 32 === 0, so each random byte modulo-32 is a uniform draw on
91
+ // the alphabet (no rejection sampling needed for the default).
92
+ var DEFAULT_ALPHABET = "23456789ABCDEFGHJKLMNPQRSTUVWXYZ";
93
+
94
+ var STATUSES = ["active", "voided", "exhausted"];
95
+
96
+ // Operator-facing caps. The mint loop is bounded — a 10M-code batch
97
+ // would dwarf the call timeout long before it failed any other gate.
98
+ var MIN_COUNT = 1;
99
+ var MAX_COUNT = 1000000;
100
+ var MIN_LENGTH = 4;
101
+ var MAX_LENGTH = 64;
102
+ var MAX_ALPHABET = 256;
103
+ var MIN_ALPHABET = 2;
104
+ var MAX_LABEL_LEN = 200;
105
+ var MAX_AFFIX_LEN = 32;
106
+ var MAX_REASON_LEN = 500;
107
+ var MAX_LIST_LIMIT = 500;
108
+
109
+ // Per-code collision retry budget. Even at 10M codes against a 32^10
110
+ // (≈1.1e15) space the birthday-collision rate inside a single batch
111
+ // stays below 1e-4; the budget protects against pathological alphabet
112
+ // + length combinations + cross-batch collisions.
113
+ var COLLISION_RETRY_BUDGET = 16;
114
+
115
+ // Random-byte draw size per attempt. Drawing a generous block per
116
+ // code amortizes the `generateBytes` call across the rejection-
117
+ // sampling loop without burning entropy.
118
+ function _bytesPerAttempt(length) { return length * 4; }
119
+
120
+ // ---- input validators ---------------------------------------------------
121
+
122
+ function _label(s) {
123
+ if (typeof s !== "string" || !s.length || s.length > MAX_LABEL_LEN) {
124
+ throw new TypeError("codeMinter: batch_label must be a non-empty string ≤ " + MAX_LABEL_LEN + " chars");
125
+ }
126
+ if (/[\x00-\x1f\x7f]/.test(s)) {
127
+ throw new TypeError("codeMinter: batch_label must not contain control bytes");
128
+ }
129
+ return s;
130
+ }
131
+
132
+ function _count(n) {
133
+ if (typeof n !== "number" || !Number.isInteger(n) || n < MIN_COUNT || n > MAX_COUNT) {
134
+ throw new TypeError("codeMinter: count must be an integer in [" + MIN_COUNT + ", " + MAX_COUNT + "]");
135
+ }
136
+ return n;
137
+ }
138
+
139
+ function _length(n) {
140
+ if (typeof n !== "number" || !Number.isInteger(n) || n < MIN_LENGTH || n > MAX_LENGTH) {
141
+ throw new TypeError("codeMinter: length must be an integer in [" + MIN_LENGTH + ", " + MAX_LENGTH + "]");
142
+ }
143
+ return n;
144
+ }
145
+
146
+ function _alphabet(a) {
147
+ if (a == null) return DEFAULT_ALPHABET;
148
+ if (typeof a !== "string" || a.length < MIN_ALPHABET || a.length > MAX_ALPHABET) {
149
+ throw new TypeError("codeMinter: alphabet must be a string of " + MIN_ALPHABET + "-" + MAX_ALPHABET + " characters");
150
+ }
151
+ // Refuse control bytes + duplicate glyphs — a duplicate biases the
152
+ // mint distribution + makes the operator-visible alphabet a lie.
153
+ if (/[\x00-\x1f\x7f]/.test(a)) {
154
+ throw new TypeError("codeMinter: alphabet must not contain control bytes");
155
+ }
156
+ var seen = Object.create(null);
157
+ for (var i = 0; i < a.length; i += 1) {
158
+ var ch = a.charAt(i);
159
+ if (seen[ch]) {
160
+ throw new TypeError("codeMinter: alphabet must not contain duplicate characters");
161
+ }
162
+ seen[ch] = true;
163
+ }
164
+ return a;
165
+ }
166
+
167
+ function _affix(s, label) {
168
+ if (s == null) return "";
169
+ if (typeof s !== "string" || s.length > MAX_AFFIX_LEN) {
170
+ throw new TypeError("codeMinter: " + label + " must be a string ≤ " + MAX_AFFIX_LEN + " chars");
171
+ }
172
+ if (/[\x00-\x1f\x7f]/.test(s)) {
173
+ throw new TypeError("codeMinter: " + label + " must not contain control bytes");
174
+ }
175
+ return s;
176
+ }
177
+
178
+ function _reason(s) {
179
+ if (typeof s !== "string" || !s.length || s.length > MAX_REASON_LEN) {
180
+ throw new TypeError("codeMinter: reason must be a non-empty string ≤ " + MAX_REASON_LEN + " chars");
181
+ }
182
+ if (/[\x00-\x1f\x7f]/.test(s)) {
183
+ throw new TypeError("codeMinter: reason must not contain control bytes");
184
+ }
185
+ return s;
186
+ }
187
+
188
+ function _batchId(s) {
189
+ if (typeof s !== "string" || !s.length) {
190
+ throw new TypeError("codeMinter: batch_id must be a non-empty string");
191
+ }
192
+ return s;
193
+ }
194
+
195
+ function _status(s) {
196
+ if (typeof s !== "string" || STATUSES.indexOf(s) === -1) {
197
+ throw new TypeError("codeMinter: status must be one of " + STATUSES.join(", "));
198
+ }
199
+ return s;
200
+ }
201
+
202
+ function _now() { return Date.now(); }
203
+
204
+ // ---- random-code generator ---------------------------------------------
205
+
206
+ // Draw one code from the alphabet using rejection sampling when
207
+ // 256 % alphabet.length !== 0. The default 32-glyph alphabet hits the
208
+ // fast path (no rejections); custom alphabets pay the rejection cost
209
+ // proportional to the modulo tail size.
210
+ function _drawCode(alphabet, length) {
211
+ var alen = alphabet.length;
212
+ var fastPath = (256 % alen) === 0;
213
+ var ceiling = fastPath ? 256 : (Math.floor(256 / alen) * alen);
214
+ var out = "";
215
+ var pos = 0;
216
+ var buf = null;
217
+ var bufLen = 0;
218
+ while (out.length < length) {
219
+ if (buf == null || pos >= bufLen) {
220
+ buf = _b().crypto.generateBytes(_bytesPerAttempt(length));
221
+ bufLen = buf.length;
222
+ pos = 0;
223
+ }
224
+ var byte = buf[pos];
225
+ pos += 1;
226
+ if (!fastPath && byte >= ceiling) {
227
+ continue; // reject tail draws to keep the distribution uniform
228
+ }
229
+ out += alphabet.charAt(byte % alen);
230
+ }
231
+ return out;
232
+ }
233
+
234
+ // ---- factory ------------------------------------------------------------
235
+
236
+ function create(opts) {
237
+ opts = opts || {};
238
+ if (!opts.coupons || typeof opts.coupons.create !== "function" || typeof opts.coupons.archive !== "function") {
239
+ throw new TypeError("codeMinter.create: opts.coupons must expose create + archive");
240
+ }
241
+ var coupons = opts.coupons;
242
+ var query = opts.query;
243
+ if (!query) {
244
+ query = function (sql, params) { return _b().externalDb.query(sql, params); };
245
+ }
246
+
247
+ async function _insertMember(memberId, batchId, code, ts) {
248
+ // Returns true on success, false on UNIQUE violation. Any other
249
+ // storage error rethrows.
250
+ try {
251
+ await query(
252
+ "INSERT INTO code_batch_members (id, batch_id, coupon_code, minted_at) VALUES (?1, ?2, ?3, ?4)",
253
+ [memberId, batchId, code, ts],
254
+ );
255
+ return true;
256
+ } catch (e) {
257
+ var msg = (e && e.message) || "";
258
+ if (/unique/i.test(msg)) return false;
259
+ throw e;
260
+ }
261
+ }
262
+
263
+ return {
264
+ DEFAULT_ALPHABET: DEFAULT_ALPHABET,
265
+ STATUSES: STATUSES,
266
+
267
+ mintBatch: async function (input) {
268
+ if (!input || typeof input !== "object") {
269
+ throw new TypeError("codeMinter.mintBatch: input object required");
270
+ }
271
+ _label(input.batch_label);
272
+ _count(input.count);
273
+ _length(input.length);
274
+ var alphabet = _alphabet(input.alphabet);
275
+ var prefix = _affix(input.prefix, "prefix");
276
+ var suffix = _affix(input.suffix, "suffix");
277
+ if (!input.coupon_template || typeof input.coupon_template !== "object") {
278
+ throw new TypeError("codeMinter.mintBatch: coupon_template object required");
279
+ }
280
+ // The minter forwards `coupon_template` keys other than `code` to
281
+ // `coupons.create` verbatim — the coupons primitive owns the
282
+ // shape validation. We only refuse a caller-supplied `code` that
283
+ // would override the minter's generated value.
284
+ if (Object.prototype.hasOwnProperty.call(input.coupon_template, "code")) {
285
+ throw new TypeError("codeMinter.mintBatch: coupon_template must not include 'code' (the minter generates it)");
286
+ }
287
+
288
+ var batchId = _b().uuid.v7();
289
+ var ts = _now();
290
+ await query(
291
+ "INSERT INTO code_batches (id, label, status, count, prefix, suffix, alphabet, length, created_at) " +
292
+ "VALUES (?1, ?2, 'active', 0, ?3, ?4, ?5, ?6, ?7)",
293
+ [batchId, input.batch_label, prefix, suffix, alphabet, input.length, ts],
294
+ );
295
+
296
+ // Per-batch dedupe set short-circuits the rare in-batch
297
+ // collision before the SQL insert pays the UNIQUE check. The
298
+ // SQL UNIQUE remains the authoritative gate (cross-batch
299
+ // collisions land there).
300
+ var seen = Object.create(null);
301
+ var sampleCodes = [];
302
+ var minted = 0;
303
+
304
+ for (var i = 0; i < input.count; i += 1) {
305
+ var inserted = false;
306
+ var attempts = 0;
307
+ var code;
308
+ var lastErr;
309
+ while (!inserted && attempts < COLLISION_RETRY_BUDGET) {
310
+ attempts += 1;
311
+ var body = _drawCode(alphabet, input.length);
312
+ code = prefix + body + suffix;
313
+ if (seen[code]) { lastErr = "in_batch_collision"; continue; }
314
+ var memberId = _b().uuid.v7();
315
+ try {
316
+ inserted = await _insertMember(memberId, batchId, code, ts);
317
+ } catch (e) {
318
+ // Storage failure unrelated to UNIQUE — bubble.
319
+ throw e;
320
+ }
321
+ if (!inserted) { lastErr = "cross_batch_collision"; continue; }
322
+ seen[code] = true;
323
+ try {
324
+ await coupons.create(Object.assign({}, input.coupon_template, { code: code }));
325
+ } catch (e) {
326
+ // The coupons primitive refused (typically a duplicate
327
+ // code that lives in coupons but not in our member table,
328
+ // or a coupon_template shape error). Roll the member row
329
+ // back and surface the underlying error.
330
+ await query("DELETE FROM code_batch_members WHERE id = ?1", [memberId]);
331
+ delete seen[code];
332
+ throw e;
333
+ }
334
+ }
335
+ if (!inserted) {
336
+ var bust = new Error("codeMinter.mintBatch: collision retry budget exhausted at code " + (i + 1) + " of " + input.count + " (last cause: " + (lastErr || "unknown") + ")");
337
+ bust.code = "CODE_MINTER_COLLISION_BUDGET_EXHAUSTED";
338
+ throw bust;
339
+ }
340
+ minted += 1;
341
+ if (sampleCodes.length < 5) sampleCodes.push(code);
342
+ }
343
+
344
+ await query(
345
+ "UPDATE code_batches SET count = ?1 WHERE id = ?2",
346
+ [minted, batchId],
347
+ );
348
+
349
+ return {
350
+ batch_id: batchId,
351
+ count_minted: minted,
352
+ sample_codes: sampleCodes,
353
+ };
354
+ },
355
+
356
+ getBatch: async function (batchId) {
357
+ _batchId(batchId);
358
+ var r = await query(
359
+ "SELECT id, label, status, count, prefix, suffix, alphabet, length, void_reason, created_at, voided_at " +
360
+ "FROM code_batches WHERE id = ?1",
361
+ [batchId],
362
+ );
363
+ return r.rows.length ? r.rows[0] : null;
364
+ },
365
+
366
+ listBatches: async function (opts2) {
367
+ opts2 = opts2 || {};
368
+ var sql, params;
369
+ if (opts2.status != null) {
370
+ _status(opts2.status);
371
+ sql = "SELECT id, label, status, count, prefix, suffix, alphabet, length, void_reason, created_at, voided_at " +
372
+ "FROM code_batches WHERE status = ?1 ORDER BY created_at DESC, id DESC";
373
+ params = [opts2.status];
374
+ } else {
375
+ sql = "SELECT id, label, status, count, prefix, suffix, alphabet, length, void_reason, created_at, voided_at " +
376
+ "FROM code_batches ORDER BY created_at DESC, id DESC";
377
+ params = [];
378
+ }
379
+ var r = await query(sql, params);
380
+ return r.rows;
381
+ },
382
+
383
+ codesForBatch: async function (opts3) {
384
+ if (!opts3 || typeof opts3 !== "object") {
385
+ throw new TypeError("codeMinter.codesForBatch: input object required");
386
+ }
387
+ _batchId(opts3.batch_id);
388
+ var limit = opts3.limit != null ? opts3.limit : 100;
389
+ if (typeof limit !== "number" || !Number.isInteger(limit) || limit < 1 || limit > MAX_LIST_LIMIT) {
390
+ throw new TypeError("codeMinter.codesForBatch: limit must be an integer in [1, " + MAX_LIST_LIMIT + "]");
391
+ }
392
+ var sql = "SELECT id, batch_id, coupon_code, minted_at FROM code_batch_members WHERE batch_id = ?1";
393
+ var params = [opts3.batch_id];
394
+ if (opts3.cursor != null) {
395
+ if (typeof opts3.cursor !== "number" || !Number.isInteger(opts3.cursor) || opts3.cursor < 0) {
396
+ throw new TypeError("codeMinter.codesForBatch: cursor must be a non-negative integer epoch-ms");
397
+ }
398
+ // `id` is a UUIDv7 so the (minted_at, id) tuple is strictly
399
+ // increasing within a batch. We page on minted_at <= cursor
400
+ // with id < cursor_id; for the simpler "cursor = last
401
+ // minted_at" form we request rows with minted_at < cursor so a
402
+ // boundary on a tied timestamp doesn't double-return rows. In
403
+ // practice every member of one mintBatch call shares one
404
+ // minted_at, so the cursor for the second page falls strictly
405
+ // below it; ties only appear when an operator runs two
406
+ // mintBatch calls in the same millisecond, in which case the
407
+ // ORDER BY id tiebreak keeps the page boundary stable across
408
+ // requests.
409
+ sql += " AND minted_at < ?" + (params.length + 1);
410
+ params.push(opts3.cursor);
411
+ }
412
+ sql += " ORDER BY minted_at DESC, id DESC LIMIT ?" + (params.length + 1);
413
+ params.push(limit);
414
+ var r = await query(sql, params);
415
+ var rows = r.rows;
416
+ var nextCursor = rows.length === limit ? rows[rows.length - 1].minted_at : null;
417
+ return { rows: rows, next_cursor: nextCursor };
418
+ },
419
+
420
+ voidBatch: async function (input) {
421
+ if (!input || typeof input !== "object") {
422
+ throw new TypeError("codeMinter.voidBatch: input object required");
423
+ }
424
+ _batchId(input.batch_id);
425
+ _reason(input.reason);
426
+ var existing = await query(
427
+ "SELECT id, status FROM code_batches WHERE id = ?1",
428
+ [input.batch_id],
429
+ );
430
+ if (!existing.rows.length) {
431
+ var miss = new Error("codeMinter.voidBatch: batch_id not found");
432
+ miss.code = "CODE_BATCH_NOT_FOUND";
433
+ throw miss;
434
+ }
435
+ if (existing.rows[0].status === "voided") {
436
+ // Idempotent — operator double-clicked the void button.
437
+ return { batch_id: input.batch_id, status: "voided", archived: 0 };
438
+ }
439
+ var ts = _now();
440
+ // Walk every member and archive the backing coupon. The
441
+ // archive call is best-effort per code — a coupon that's
442
+ // already archived (because an operator hand-archived a single
443
+ // code) shouldn't block voiding the rest of the batch.
444
+ var members = await query(
445
+ "SELECT coupon_code FROM code_batch_members WHERE batch_id = ?1",
446
+ [input.batch_id],
447
+ );
448
+ var archived = 0;
449
+ for (var i = 0; i < members.rows.length; i += 1) {
450
+ var code = members.rows[i].coupon_code;
451
+ try {
452
+ await coupons.archive(code);
453
+ archived += 1;
454
+ } catch (_e) {
455
+ // Already-archived / not-found from the coupons surface is
456
+ // tolerated — the void operation's invariant is "every
457
+ // backing coupon is in the archived state when this returns
458
+ // successfully," not "this call performed every archive
459
+ // itself." A real storage failure surfaces on the next
460
+ // query below, which writes the batch state.
461
+ }
462
+ }
463
+ await query(
464
+ "UPDATE code_batches SET status = 'voided', void_reason = ?1, voided_at = ?2 WHERE id = ?3",
465
+ [input.reason, ts, input.batch_id],
466
+ );
467
+ return { batch_id: input.batch_id, status: "voided", archived: archived };
468
+ },
469
+
470
+ // Async-iterable yielding CSV chunks. First yield is the header
471
+ // row; each subsequent yield is one CRLF-terminated data row. The
472
+ // iterable walks members in (minted_at ASC, id ASC) order so two
473
+ // exports of the same batch produce byte-identical output (useful
474
+ // for re-printing a label sheet from the same source).
475
+ exportBatchCsv: function (input) {
476
+ if (!input || typeof input !== "object") {
477
+ throw new TypeError("codeMinter.exportBatchCsv: input object required");
478
+ }
479
+ _batchId(input.batch_id);
480
+ var pageSize = 500;
481
+ var batchId = input.batch_id;
482
+ var qHandle = query;
483
+ return {
484
+ [Symbol.asyncIterator]: function () {
485
+ var sentHeader = false;
486
+ var cursor = null;
487
+ var buffered = [];
488
+ var done = false;
489
+ return {
490
+ next: async function () {
491
+ if (!sentHeader) {
492
+ sentHeader = true;
493
+ return { value: "coupon_code,minted_at\r\n", done: false };
494
+ }
495
+ if (buffered.length === 0 && !done) {
496
+ var sql = "SELECT coupon_code, minted_at, id FROM code_batch_members WHERE batch_id = ?1";
497
+ var params = [batchId];
498
+ if (cursor != null) {
499
+ sql += " AND (minted_at > ?2 OR (minted_at = ?2 AND id > ?3))";
500
+ params.push(cursor.minted_at, cursor.id);
501
+ }
502
+ sql += " ORDER BY minted_at ASC, id ASC LIMIT ?" + (params.length + 1);
503
+ params.push(pageSize);
504
+ var r = await qHandle(sql, params);
505
+ if (r.rows.length < pageSize) done = true;
506
+ for (var i = 0; i < r.rows.length; i += 1) {
507
+ var row = r.rows[i];
508
+ // The coupon_code is constrained to the alphabet at
509
+ // mint time, so CSV escaping is a no-op for the
510
+ // value column — no commas, quotes, CR, or LF can
511
+ // appear. We still emit the minted_at as a bare
512
+ // integer (epoch-ms) so the CSV consumer can parse
513
+ // it without quote-stripping.
514
+ buffered.push(row.coupon_code + "," + row.minted_at + "\r\n");
515
+ cursor = { minted_at: row.minted_at, id: row.id };
516
+ }
517
+ if (r.rows.length === 0) done = true;
518
+ }
519
+ if (buffered.length) {
520
+ return { value: buffered.shift(), done: false };
521
+ }
522
+ return { value: undefined, done: true };
523
+ },
524
+ };
525
+ },
526
+ };
527
+ },
528
+ };
529
+ }
530
+
531
+ module.exports = {
532
+ create: create,
533
+ DEFAULT_ALPHABET: DEFAULT_ALPHABET,
534
+ STATUSES: STATUSES,
535
+ };