@monoflake/sdk 0.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.
Files changed (44) hide show
  1. package/LICENSE +22 -0
  2. package/dist/artifacts/src/anchors.d.ts +34 -0
  3. package/dist/artifacts/src/anchors.js +64 -0
  4. package/dist/artifacts/src/api.d.ts +14 -0
  5. package/dist/artifacts/src/api.js +20 -0
  6. package/dist/artifacts/src/batch.d.ts +105 -0
  7. package/dist/artifacts/src/batch.js +81 -0
  8. package/dist/artifacts/src/engagement.d.ts +61 -0
  9. package/dist/artifacts/src/engagement.js +67 -0
  10. package/dist/artifacts/src/feed.d.ts +42 -0
  11. package/dist/artifacts/src/feed.js +89 -0
  12. package/dist/artifacts/src/index.d.ts +4224 -0
  13. package/dist/artifacts/src/index.js +219 -0
  14. package/dist/artifacts/src/picture.d.ts +116 -0
  15. package/dist/artifacts/src/picture.js +161 -0
  16. package/dist/artifacts/src/resource.d.ts +1391 -0
  17. package/dist/artifacts/src/resource.js +477 -0
  18. package/dist/artifacts/src/schema.d.ts +5 -0
  19. package/dist/artifacts/src/schema.js +18 -0
  20. package/dist/artifacts/src/types.d.ts +396 -0
  21. package/dist/artifacts/src/types.js +0 -0
  22. package/dist/cache/src/index.d.ts +67 -0
  23. package/dist/cache/src/index.js +58 -0
  24. package/dist/imgsrc/src/index.d.ts +16 -0
  25. package/dist/imgsrc/src/index.js +89 -0
  26. package/dist/limits/src/bucket.d.ts +28 -0
  27. package/dist/limits/src/bucket.js +23 -0
  28. package/dist/limits/src/index.d.ts +29 -0
  29. package/dist/limits/src/index.js +62 -0
  30. package/dist/limits/src/key.d.ts +37 -0
  31. package/dist/limits/src/key.js +64 -0
  32. package/dist/robots/src/index.d.ts +98 -0
  33. package/dist/robots/src/index.js +196 -0
  34. package/dist/security/src/agents.d.ts +10 -0
  35. package/dist/security/src/agents.js +95 -0
  36. package/dist/security/src/index.d.ts +12 -0
  37. package/dist/security/src/index.js +37 -0
  38. package/dist/src/index.d.ts +218 -0
  39. package/dist/src/index.js +219 -0
  40. package/dist/store/src/index.d.ts +92 -0
  41. package/dist/store/src/index.js +264 -0
  42. package/dist/symlink/src/index.d.ts +24 -0
  43. package/dist/symlink/src/index.js +89 -0
  44. package/package.json +85 -0
@@ -0,0 +1,477 @@
1
+ import { byLocale, hash } from "./schema.js";
2
+ import * as v from "valibot";
3
+ //#region artifacts/src/resource.ts
4
+ /**
5
+ * The resource record: its envelope, the id that names it, and every layer a resource can carry.
6
+ * See spec/architecture/resource.md.
7
+ */
8
+ /**
9
+ * The envelope's own version, which moves only when one of its five keys moves.
10
+ *
11
+ * Adding a type, adding a layer or changing what a layer holds touches none of those names, so
12
+ * none of them is a 6. A version that rose for reasons unrelated to what a reader parses would
13
+ * teach the reader to ignore it. See spec/architecture/resource.md, "The record".
14
+ */
15
+ const RESOURCE_VERSION = 5;
16
+ /**
17
+ * A resource id: five characters of lowercase base36.
18
+ *
19
+ * The other id entirely -- `HASH_PATTERN` above identifies a run of bytes, this identifies a
20
+ * thing, and nothing converts one into the other. Five is chosen for the address space and for a
21
+ * human reading article source, not for a birthday bound: collisions are not a probability here
22
+ * because allocation checks the register first. See spec/architecture/resource.md, "Two ids".
23
+ */
24
+ const RESOURCE_PATTERN = /^[0-9a-z]{5}$/;
25
+ function isResourceId(value) {
26
+ return RESOURCE_PATTERN.test(value);
27
+ }
28
+ /** A rid as every schema here spells one, beside `hash`, which is the other id. */
29
+ const rid = v.pipe(v.string(), v.regex(RESOURCE_PATTERN));
30
+ /** `media.image.photo`: segments read left to right, each one a key in the register below. */
31
+ const namespace = v.pipe(v.string(), v.regex(/^[a-z]+(?:\.[a-z]+)*$/));
32
+ /**
33
+ * What a layer carries whatever it is, checked before the layer's own schema runs.
34
+ *
35
+ * The number is per layer rather than on the envelope so a layer that changes shape raises its
36
+ * own and nothing above it notices.
37
+ */
38
+ const layered = { version: v.number() };
39
+ /**
40
+ * What a resource is derived from, including bytes nobody can fetch.
41
+ *
42
+ * A list, because re-scanning a subject adds an origin to the resource rather than making a
43
+ * second one. The originals are never published; the cid is kept so the next import of the same
44
+ * file is recognised and skipped. See spec/architecture/resource.md, "`source` and `origin`".
45
+ */
46
+ const MediaLayerSchema = v.object({
47
+ ...layered,
48
+ origin: v.array(v.object({
49
+ blake3: hash,
50
+ mime: v.string(),
51
+ bytes: v.number()
52
+ }))
53
+ });
54
+ /** The mimes that serve any size, named once so no caller has to repeat the condition. */
55
+ const SCALABLE_MIMES = /* @__PURE__ */ new Set(["image/svg+xml"]);
56
+ /**
57
+ * One published encoding of a picture.
58
+ *
59
+ * `resolution` is absent on a vector for the same reason the layer's is: there are no pixels to
60
+ * report, and a caller that asks and receives nothing has its answer. `quality` is the normalised
61
+ * encoder setting, kept because re-deriving has to reproduce what was published.
62
+ */
63
+ const ImageVariantSchema = v.object({
64
+ content: hash,
65
+ mime: v.string(),
66
+ bytes: v.number(),
67
+ resolution: v.optional(v.object({
68
+ width: v.number(),
69
+ height: v.number()
70
+ })),
71
+ quality: v.optional(v.number())
72
+ });
73
+ /**
74
+ * Two facts about a picture, and only one of them is universal.
75
+ *
76
+ * `dimension` is the intrinsic box -- an SVG's `viewBox`, a bitmap's pixels -- and is what layout
77
+ * is computed from. `resolution` is actual pixels and bitmaps only: **absent is the answer**, so
78
+ * a vector needs no second field saying it is one. The variants bind here because this is the
79
+ * first layer at which content is a fact. See spec/architecture/resource.md, "four steps".
80
+ */
81
+ const ImageLayerSchema = v.object({
82
+ ...layered,
83
+ /**
84
+ * Base64 thumbhash: the compact canonical placeholder, and what `placeholder` decodes from.
85
+ *
86
+ * Optional, because a picture is not the only thing this layer describes. An icon binds two
87
+ * files under `icon` and has no single picture to stand in for -- **absent is the answer**
88
+ * there, exactly as it is for `resolution`, and a placeholder invented for one of the two
89
+ * tones would be painted under the other.
90
+ */
91
+ thumbhash: v.optional(v.string()),
92
+ /**
93
+ * The same placeholder decoded, as a `data:image/webp` URI a page paints directly.
94
+ *
95
+ * Carried rather than derived where it is wanted, and the reason is not size: this record is
96
+ * read by a **universal** load, and the decode is a WebP codec reached through `node:fs`,
97
+ * which no browser bundle may contain. One decode at import serves both halves. Measured at
98
+ * 167 characters median here, about 8% of a photograph's record. Additive, so the layer
99
+ * keeps its version -- see the parsing table in spec/architecture/resource.md.
100
+ */
101
+ placeholder: v.optional(v.string()),
102
+ dimension: v.object({
103
+ width: v.number(),
104
+ height: v.number(),
105
+ aspect: v.string()
106
+ }),
107
+ resolution: v.optional(v.object({
108
+ width: v.number(),
109
+ height: v.number()
110
+ })),
111
+ variants: v.array(ImageVariantSchema)
112
+ });
113
+ /**
114
+ * What a sensor did, which is worked out once at import and never again.
115
+ *
116
+ * Every field is optional and nothing here is trusted about the file: dimensions come from
117
+ * decoding, and `address` is not in the file at all -- it is looked up offline from `location`.
118
+ * See web's spec/architecture/media.md, "Where a photograph was taken is worked out offline".
119
+ *
120
+ * Spread into two layers rather than named as one, because `services/apps/local/src/image/exif.rs`
121
+ * is flattened into both and a wrapper here would be a key the Rust side never writes.
122
+ */
123
+ const exif = {
124
+ captured: v.optional(v.string()),
125
+ camera: v.optional(v.object({
126
+ model: v.optional(v.string()),
127
+ manufacturer: v.optional(v.string())
128
+ })),
129
+ lens: v.optional(v.object({
130
+ model: v.optional(v.string()),
131
+ manufacturer: v.optional(v.string()),
132
+ focal_length: v.optional(v.number()),
133
+ focal_length_35mm: v.optional(v.number()),
134
+ f_number: v.optional(v.number())
135
+ })),
136
+ exposure: v.optional(v.object({
137
+ time: v.optional(v.string()),
138
+ iso: v.optional(v.number()),
139
+ bias_ev: v.optional(v.number()),
140
+ mode: v.optional(v.string()),
141
+ program: v.optional(v.string()),
142
+ metering: v.optional(v.string()),
143
+ white_balance: v.optional(v.string()),
144
+ flash: v.optional(v.boolean())
145
+ })),
146
+ location: v.optional(v.object({
147
+ latitude: v.optional(v.number()),
148
+ longitude: v.optional(v.number()),
149
+ altitude: v.optional(v.number()),
150
+ accuracy: v.optional(v.number()),
151
+ direction: v.optional(v.number())
152
+ })),
153
+ address: v.optional(v.object({
154
+ continent: v.optional(v.string()),
155
+ country: v.optional(v.string()),
156
+ country_code: v.optional(v.string()),
157
+ region: v.optional(v.string()),
158
+ subregion: v.optional(v.string()),
159
+ city: v.optional(v.string()),
160
+ district: v.optional(v.string()),
161
+ postal_code: v.optional(v.string()),
162
+ timezone: v.optional(v.string())
163
+ })),
164
+ software: v.optional(v.string()),
165
+ color_space: v.optional(v.string()),
166
+ /** Read and honoured on the way in: ignoring it turns every derived image. */
167
+ orientation: v.optional(v.number())
168
+ };
169
+ /** A camera pointed at the world, which is the whole of what this layer brings. */
170
+ const PhotoLayerSchema = v.object({
171
+ ...layered,
172
+ ...exif
173
+ });
174
+ /**
175
+ * A capture of a screen, which is what a picture with no camera turns out to be.
176
+ *
177
+ * The scale is what earns this its own layer: a screenshot is taken at a device pixel ratio, and
178
+ * without it nothing can say whether a 2560px capture is a wide screen or a retina one. The rest
179
+ * is the same account a photograph carries -- ten records here hold `color_space` and `software`,
180
+ * and a layer with no home for them would drop them on the way past.
181
+ */
182
+ const ScreenshotLayerSchema = v.object({
183
+ ...layered,
184
+ scale: v.optional(v.number()),
185
+ ...exif
186
+ });
187
+ /**
188
+ * A still cut from a clip, pointing back at what it was cut from.
189
+ *
190
+ * The clip's `cover` points here and this points there, and that is not a cycle to remove:
191
+ * replacing the cover leaves this a frame of that clip. See spec/architecture/resource.md,
192
+ * "References resolve lazily".
193
+ */
194
+ const FrameLayerSchema = v.object({
195
+ ...layered,
196
+ source: rid,
197
+ /** Seconds into the clip, which is the one thing the picture itself cannot say. */
198
+ at: v.optional(v.number())
199
+ });
200
+ /** Which shade an icon is drawn for. Closed, and the axis this layer exists to carry. */
201
+ const TONES = ["light", "dark"];
202
+ /**
203
+ * Another site's mark: one resource per domain, one file per tone.
204
+ *
205
+ * Light and dark are two pictures, not two encodings of one, so they bind here and not in
206
+ * `image.variants`. Each is described as an image variant is; what differs is the axis. A site
207
+ * with one mark carries one key, and absence is the answer as it is for `resolution`. See
208
+ * spec/architecture/resource.md, "Content binds at the layer that has it".
209
+ */
210
+ const IconLayerSchema = v.pipe(v.object({
211
+ ...layered,
212
+ domain: v.string(),
213
+ tones: v.object({
214
+ light: v.optional(ImageVariantSchema),
215
+ dark: v.optional(ImageVariantSchema)
216
+ })
217
+ }), v.check((icon) => TONES.some((tone) => icon.tones[tone]), "an icon names no file for any tone"));
218
+ /**
219
+ * This site's own mark: one thing, six files.
220
+ *
221
+ * A map under fixed names rather than a variant list, because each of these is asked for by name
222
+ * -- a favicon, a touch icon, a BIMI mark -- and never by size. See
223
+ * spec/architecture/delivery.md, "A name is resolved, never stored".
224
+ */
225
+ const MarkLayerSchema = v.object({
226
+ ...layered,
227
+ files: v.record(v.string(), v.object({
228
+ content: hash,
229
+ mime: v.string(),
230
+ bytes: v.number()
231
+ }))
232
+ });
233
+ /**
234
+ * A moving picture: what the source was, what was published of it, and what is read over it.
235
+ *
236
+ * The source numbers are kept because a rung is a re-encode and none of them can be read back off
237
+ * one. `codec` is the full RFC 6381 string and names every track, since that is what a browser
238
+ * reads to decide whether it can play the file at all. See web's
239
+ * spec/architecture/video/pipeline.md.
240
+ */
241
+ const VideoLayerSchema = v.object({
242
+ ...layered,
243
+ source: v.object({
244
+ mime: v.string(),
245
+ width: v.number(),
246
+ height: v.number(),
247
+ aspect: v.string(),
248
+ bytes: v.number(),
249
+ duration: v.number(),
250
+ frame_rate: v.number(),
251
+ frames: v.number(),
252
+ audio: v.boolean(),
253
+ loudness: v.optional(v.number()),
254
+ peak: v.optional(v.number())
255
+ }),
256
+ /** The poster frame, which is a resource of its own because it is referred to from two places. */
257
+ cover: rid,
258
+ variants: v.array(v.object({
259
+ content: hash,
260
+ mime: v.string(),
261
+ bytes: v.number(),
262
+ resolution: v.object({
263
+ width: v.number(),
264
+ height: v.number()
265
+ }),
266
+ codec: v.string()
267
+ })),
268
+ tracks: v.array(v.object({
269
+ content: hash,
270
+ mime: v.string(),
271
+ language: v.string(),
272
+ kind: v.picklist([
273
+ "captions",
274
+ "subtitles",
275
+ "descriptions"
276
+ ]),
277
+ bytes: v.number()
278
+ }))
279
+ });
280
+ /**
281
+ * A clip, which is a video cut from a longer one.
282
+ *
283
+ * The excerpt is what the layer brings: seconds into the original, which no derived file records
284
+ * and which is the difference between a clip and the thing it came out of.
285
+ */
286
+ const ClipLayerSchema = v.object({
287
+ ...layered,
288
+ excerpt: v.optional(v.object({
289
+ from: v.number(),
290
+ to: v.number()
291
+ }))
292
+ });
293
+ /**
294
+ * Written prose: one source, and the views compiled out of it.
295
+ *
296
+ * The nine locale bodies are what the document is made of rather than nine things, so they bind
297
+ * here and none of them has a rid. `slug` is the identity and the address is the root's to say.
298
+ * See spec/architecture/resource.md, "The catalogue".
299
+ */
300
+ const DocumentLayerSchema = v.object({
301
+ ...layered,
302
+ slug: v.string(),
303
+ source: hash,
304
+ locales: byLocale(v.object({
305
+ content: hash,
306
+ card: v.optional(hash),
307
+ /** False when this locale is showing the source as a safe fallback. */
308
+ translated: v.boolean()
309
+ }))
310
+ });
311
+ /** An article: the document, plus the two things a page without a date does not carry. */
312
+ const ArticleLayerSchema = v.object({
313
+ ...layered,
314
+ dates: v.object({
315
+ created: v.string(),
316
+ published: v.optional(v.string()),
317
+ lastmod: v.string()
318
+ }),
319
+ tags: v.array(v.string())
320
+ });
321
+ /**
322
+ * The attribution text, which is the one document nobody writes.
323
+ *
324
+ * Rewritten whenever the dependency tree moves, so when it was last derived is the fact that
325
+ * distinguishes it from prose someone sat down and wrote.
326
+ */
327
+ const NoticeLayerSchema = v.object({
328
+ ...layered,
329
+ generated: v.string()
330
+ });
331
+ /**
332
+ * The register a `type` segment selects a parser from, and the highest version each build reads.
333
+ *
334
+ * The version is held beside the schema rather than inside it: a layer numbered above what is
335
+ * here is a shape from a newer producer, and the answer is to stop at that segment rather than to
336
+ * fail the whole record. See spec/architecture/resource.md, "Parsing is optimistic".
337
+ */
338
+ const LAYERS = {
339
+ media: {
340
+ version: 1,
341
+ schema: MediaLayerSchema
342
+ },
343
+ image: {
344
+ version: 1,
345
+ schema: ImageLayerSchema
346
+ },
347
+ photo: {
348
+ version: 1,
349
+ schema: PhotoLayerSchema
350
+ },
351
+ screenshot: {
352
+ version: 1,
353
+ schema: ScreenshotLayerSchema
354
+ },
355
+ frame: {
356
+ version: 1,
357
+ schema: FrameLayerSchema
358
+ },
359
+ icon: {
360
+ version: 1,
361
+ schema: IconLayerSchema
362
+ },
363
+ mark: {
364
+ version: 1,
365
+ schema: MarkLayerSchema
366
+ },
367
+ video: {
368
+ version: 1,
369
+ schema: VideoLayerSchema
370
+ },
371
+ clip: {
372
+ version: 1,
373
+ schema: ClipLayerSchema
374
+ },
375
+ document: {
376
+ version: 1,
377
+ schema: DocumentLayerSchema
378
+ },
379
+ article: {
380
+ version: 1,
381
+ schema: ArticleLayerSchema
382
+ },
383
+ notice: {
384
+ version: 1,
385
+ schema: NoticeLayerSchema
386
+ }
387
+ };
388
+ function isLayerName(value) {
389
+ return Object.hasOwn(LAYERS, value);
390
+ }
391
+ /**
392
+ * What a bare resource id means, written as a scheme rather than as an address.
393
+ *
394
+ * An absolute URL here would bake a hostname into every record, so changing one would mean
395
+ * rewriting all of them. A scheme is expanded by whoever answers, from `@monoflake/sdk`, which is
396
+ * the one place a hostname is declared. `services/libs/fonts` already does this with `__CDN_URL__`.
397
+ */
398
+ const CANONICAL_PATTERN = /^(?:cid:[0-9a-f]{32}\.[a-z0-9]+|slug:[a-z0-9][a-z0-9-]*)$/;
399
+ /**
400
+ * Six fields and a container, which is the whole record.
401
+ *
402
+ * `layers` is read loosely here on purpose: the envelope's job is to find the layers and say how
403
+ * old each one is, and which schema a layer is then read under is its segment's to decide. A
404
+ * strict object at this level would strip every field the register is about to ask for.
405
+ */
406
+ const ResourceSchema = v.object({
407
+ version: v.literal(5),
408
+ resource: rid,
409
+ type: namespace,
410
+ created: v.string(),
411
+ updated: v.string(),
412
+ canonical: v.optional(v.pipe(v.string(), v.regex(CANONICAL_PATTERN))),
413
+ layers: v.record(v.string(), v.looseObject(layered))
414
+ });
415
+ /**
416
+ * A canonical scheme expanded against the hosts this deployment knows.
417
+ *
418
+ * `cid:` names an object and `slug:` names an article, and neither carries a hostname -- which is
419
+ * what lets a domain move without a record being touched. A slug needs no lookup: the site
420
+ * resolves a bare name to the article's real path itself.
421
+ */
422
+ function expandCanonical(canonical, hosts) {
423
+ const object = canonical.startsWith("cid:") ? canonical.slice(4) : void 0;
424
+ if (object) return `${hosts.cdn}/object/${object}`;
425
+ const slug = canonical.startsWith("slug:") ? canonical.slice(5) : void 0;
426
+ return slug ? `${hosts.site}/${slug}` : void 0;
427
+ }
428
+ /** The segments of a type, in the order they narrow. `media.image.photo` is three claims. */
429
+ function parseType(type) {
430
+ return type.split(".").filter((segment) => segment.length > 0);
431
+ }
432
+ /**
433
+ * Read a record for as much of it as this build knows, and stop rather than fall over.
434
+ *
435
+ * Both sides of a change are pushed together and the only skew is minutes, which does not justify
436
+ * a compatibility contract -- it justifies not falling over. So an unknown segment or a layer
437
+ * numbered too high ends the chain and keeps what came before it. See
438
+ * spec/architecture/resource.md, "Parsing is optimistic, not compatible".
439
+ */
440
+ function parseResource(value) {
441
+ const record = v.parse(ResourceSchema, value);
442
+ const declared = parseType(record.type);
443
+ for (const held of Object.keys(record.layers)) if (!declared.includes(held)) throw new Error(`resource ${record.resource} holds layer ${held}, absent from ${record.type}`);
444
+ const layers = {};
445
+ const segments = [];
446
+ for (const segment of declared) {
447
+ if (!isLayerName(segment)) break;
448
+ const known = LAYERS[segment];
449
+ const held = record.layers[segment];
450
+ if (!held || held.version > known.version) break;
451
+ layers[segment] = v.parse(known.schema, held);
452
+ segments.push(segment);
453
+ }
454
+ return {
455
+ resource: record.resource,
456
+ type: record.type,
457
+ created: record.created,
458
+ updated: record.updated,
459
+ segments,
460
+ layers
461
+ };
462
+ }
463
+ /**
464
+ * The layer a caller cannot proceed without, or an error naming what it got instead.
465
+ *
466
+ * The one strict reading in the file, and the difference is worth stating: everything above is "I
467
+ * do not know about this", which is survivable, and this is "this is not the thing you asked
468
+ * for", which is not. Answering it with a blank is how a missing image becomes a missing image
469
+ * nobody reports. See spec/architecture/resource.md, "Parsing is optimistic, not compatible".
470
+ */
471
+ function requireSegment(resource, segment) {
472
+ const layer = resource.layers[segment];
473
+ if (!layer) throw new Error(`resource ${resource.resource} is ${resource.type}, asked for ${segment}`);
474
+ return layer;
475
+ }
476
+ //#endregion
477
+ export { ArticleLayerSchema, CANONICAL_PATTERN, ClipLayerSchema, DocumentLayerSchema, FrameLayerSchema, IconLayerSchema, ImageLayerSchema, ImageVariantSchema, LAYERS, MarkLayerSchema, MediaLayerSchema, NoticeLayerSchema, PhotoLayerSchema, RESOURCE_PATTERN, RESOURCE_VERSION, ResourceSchema, SCALABLE_MIMES, ScreenshotLayerSchema, TONES, VideoLayerSchema, expandCanonical, isLayerName, isResourceId, parseResource, parseType, requireSegment };
@@ -0,0 +1,5 @@
1
+ import "valibot";
2
+ //#region artifacts/src/schema.d.ts
3
+ /** BLAKE3 truncated to 128 bits, as the bucket spells it. See spec/architecture/artifacts.md. */
4
+ export declare const HASH_PATTERN: RegExp;
5
+ //#endregion
@@ -0,0 +1,18 @@
1
+ import * as v from "valibot";
2
+ import { LOCALE_CODES } from "@canmi/me/locales";
3
+ //#region artifacts/src/schema.ts
4
+ /**
5
+ * The pieces every schema in this library is built from: a content id, and a value kept once per
6
+ * locale. A module of its own so `index.ts`, `resource.ts` and `picture.ts` can share them without
7
+ * importing one another in a circle.
8
+ */
9
+ /** BLAKE3 truncated to 128 bits, as the bucket spells it. See spec/architecture/artifacts.md. */
10
+ const HASH_PATTERN = /^[0-9a-f]{32}$/;
11
+ /** A content id as every schema here spells one. */
12
+ const hash = v.pipe(v.string(), v.regex(HASH_PATTERN));
13
+ function byLocale(value) {
14
+ const entries = LOCALE_CODES.map((code) => [code, value]);
15
+ return v.partial(v.object(Object.fromEntries(entries)));
16
+ }
17
+ //#endregion
18
+ export { HASH_PATTERN, byLocale, hash };