@wemush/wols 1.2.0 → 1.3.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/CHANGELOG.md +170 -0
- package/README.md +204 -31
- package/dist/cjs/compact-url.d.ts +150 -0
- package/dist/cjs/compact-url.d.ts.map +1 -0
- package/dist/cjs/crypto/decrypt.d.ts +110 -0
- package/dist/cjs/crypto/decrypt.d.ts.map +1 -0
- package/dist/cjs/crypto/decrypt.js +519 -99
- package/dist/cjs/crypto/decrypt.js.map +1 -1
- package/dist/cjs/crypto/encrypt.d.ts +132 -0
- package/dist/cjs/crypto/encrypt.d.ts.map +1 -0
- package/dist/cjs/crypto/encrypt.js +281 -200
- package/dist/cjs/crypto/encrypt.js.map +1 -1
- package/dist/cjs/crypto/index.d.ts +12 -0
- package/dist/cjs/crypto/index.d.ts.map +1 -0
- package/dist/cjs/crypto/index.js +15 -2
- package/dist/cjs/crypto/index.js.map +1 -1
- package/dist/cjs/crypto/shared.d.ts +116 -0
- package/dist/cjs/crypto/shared.d.ts.map +1 -0
- package/dist/cjs/crypto/shared.js +238 -0
- package/dist/cjs/crypto/shared.js.map +1 -0
- package/dist/cjs/errors.d.ts +133 -0
- package/dist/cjs/errors.d.ts.map +1 -0
- package/dist/cjs/errors.js +39 -1
- package/dist/cjs/errors.js.map +1 -1
- package/dist/cjs/index.d.ts +29 -0
- package/dist/cjs/index.d.ts.map +1 -0
- package/dist/cjs/index.js +19 -4
- package/dist/cjs/index.js.map +1 -1
- package/dist/cjs/package.json +3 -0
- package/dist/cjs/parser.d.ts +30 -0
- package/dist/cjs/parser.d.ts.map +1 -0
- package/dist/cjs/qr/generate.d.ts +62 -0
- package/dist/cjs/qr/generate.d.ts.map +1 -0
- package/dist/cjs/qr/index.d.ts +10 -0
- package/dist/cjs/qr/index.d.ts.map +1 -0
- package/dist/cjs/specimen.d.ts +96 -0
- package/dist/cjs/specimen.d.ts.map +1 -0
- package/dist/cjs/specimen.js +84 -11
- package/dist/cjs/specimen.js.map +1 -1
- package/dist/cjs/types.d.ts +735 -0
- package/dist/cjs/types.d.ts.map +1 -0
- package/dist/cjs/types.js +65 -5
- package/dist/cjs/types.js.map +1 -1
- package/dist/cjs/utils/cuid.d.ts +46 -0
- package/dist/cjs/utils/cuid.d.ts.map +1 -0
- package/dist/cjs/utils/cuid.js +6 -4
- package/dist/cjs/utils/cuid.js.map +1 -1
- package/dist/cjs/utils/environment.d.ts +137 -0
- package/dist/cjs/utils/environment.d.ts.map +1 -0
- package/dist/cjs/utils/iso8601.d.ts +79 -0
- package/dist/cjs/utils/iso8601.d.ts.map +1 -0
- package/dist/cjs/utils/migration.d.ts +162 -0
- package/dist/cjs/utils/migration.d.ts.map +1 -0
- package/dist/cjs/validator.d.ts +25 -0
- package/dist/cjs/validator.d.ts.map +1 -0
- package/dist/cjs/validator.js +33 -22
- package/dist/cjs/validator.js.map +1 -1
- package/dist/esm/crypto/decrypt.js +515 -99
- package/dist/esm/crypto/decrypt.js.map +1 -1
- package/dist/esm/crypto/encrypt.js +277 -200
- package/dist/esm/crypto/encrypt.js.map +1 -1
- package/dist/esm/crypto/index.js +5 -3
- package/dist/esm/crypto/index.js.map +1 -1
- package/dist/esm/crypto/shared.js +222 -0
- package/dist/esm/crypto/shared.js.map +1 -0
- package/dist/esm/errors.js +37 -0
- package/dist/esm/errors.js.map +1 -1
- package/dist/esm/index.js +6 -6
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/specimen.js +84 -12
- package/dist/esm/specimen.js.map +1 -1
- package/dist/esm/types.js +63 -4
- package/dist/esm/types.js.map +1 -1
- package/dist/esm/utils/cuid.js +6 -4
- package/dist/esm/utils/cuid.js.map +1 -1
- package/dist/esm/validator.js +34 -23
- package/dist/esm/validator.js.map +1 -1
- package/dist/types/crypto/decrypt.d.ts +93 -22
- package/dist/types/crypto/decrypt.d.ts.map +1 -1
- package/dist/types/crypto/encrypt.d.ts +106 -61
- package/dist/types/crypto/encrypt.d.ts.map +1 -1
- package/dist/types/crypto/index.d.ts +5 -3
- package/dist/types/crypto/index.d.ts.map +1 -1
- package/dist/types/crypto/shared.d.ts +116 -0
- package/dist/types/crypto/shared.d.ts.map +1 -0
- package/dist/types/errors.d.ts +31 -1
- package/dist/types/errors.d.ts.map +1 -1
- package/dist/types/index.d.ts +7 -7
- package/dist/types/index.d.ts.map +1 -1
- package/dist/types/specimen.d.ts +29 -4
- package/dist/types/specimen.d.ts.map +1 -1
- package/dist/types/types.d.ts +265 -40
- package/dist/types/types.d.ts.map +1 -1
- package/dist/types/utils/cuid.d.ts +5 -3
- package/dist/types/utils/cuid.d.ts.map +1 -1
- package/dist/types/validator.d.ts +1 -1
- package/dist/types/validator.d.ts.map +1 -1
- package/package.json +32 -14
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to `@wemush/wols` are documented here. This project follows
|
|
4
|
+
[Semantic Versioning](https://semver.org/), except that its major and minor versions match the
|
|
5
|
+
WOLS specification version it implements. A spec minor release can require breaking changes, and
|
|
6
|
+
those are listed below.
|
|
7
|
+
|
|
8
|
+
## [1.3.0]
|
|
9
|
+
|
|
10
|
+
Brings the library in line with the [WOLS 1.3.0 specification](https://github.com/wemush/open-standard/blob/main/SPECIFICATION.md).
|
|
11
|
+
Implements WOLS `1.3.0-rc.2` (open-standard tag `1.3.0-rc.2`, commit `9d200c9`). All `MUST` and `SHOULD` cases in the shared conformance vectors pass:
|
|
12
|
+
|
|
13
|
+
| Vector file | MUST | SHOULD |
|
|
14
|
+
| ----------------- | ----- | ------ |
|
|
15
|
+
| `embedded.json` | 12/12 | 1/1 |
|
|
16
|
+
| `primordia.json` | 4/4 | 1/1 |
|
|
17
|
+
| `generation.json` | 9/9 | 1/1 |
|
|
18
|
+
| `encrypted.json` | 19/19 | 2/2 |
|
|
19
|
+
| `field-encryption.json` | 6/6 | — |
|
|
20
|
+
|
|
21
|
+
### WOLS 1.3.0-rc.2 encryption rules
|
|
22
|
+
|
|
23
|
+
These rules were added on top of `rc.1` before 1.3.0 was published. Field-level ciphertexts made by
|
|
24
|
+
earlier unpublished 1.3.0 builds don't decrypt under them. Because nothing was released, no
|
|
25
|
+
migration is needed.
|
|
26
|
+
|
|
27
|
+
- **NFC passphrases.** The passphrase is normalized to Unicode NFC, then UTF-8 encoded, before
|
|
28
|
+
PBKDF2. This applies to encryption, decryption and `derivePassphraseKey`.
|
|
29
|
+
- **Field-level AAD.** Each encrypted field is bound to its label and field name. The AES-GCM
|
|
30
|
+
additional data is the UTF-8 of `"{id}\n{field}"`, and every field gets its own random IV.
|
|
31
|
+
Values that are swapped between fields or copied from another label fail authentication.
|
|
32
|
+
`decryptField` now takes a third argument, `{ id, field }`. The URL format has no AAD and keeps
|
|
33
|
+
the decrypted-`id` check.
|
|
34
|
+
- **Identifying fields.** `@context`, `@type`, `id`, `version`, `type` and `species` are never
|
|
35
|
+
encrypted. `decryptFields` refuses a label that has an encrypted identifying field, with reason
|
|
36
|
+
`identifying-field`.
|
|
37
|
+
- **Iterations.**
|
|
38
|
+
- Generation defaults to 600,000 iterations. Anything from 100,000 to 10,000,000 is allowed.
|
|
39
|
+
- Decoding rejects fewer than 10,000 or more than 10,000,000, before deriving any key.
|
|
40
|
+
`maxIterations` may be set lower, never higher.
|
|
41
|
+
- For field-level decryption, the 10,000,000 limit applies to the label's total across distinct
|
|
42
|
+
salt and iteration-count pairs.
|
|
43
|
+
- **ID path encoding.** Malformed percent-escapes and invalid UTF-8 in the URL path are rejected
|
|
44
|
+
with reason `invalid-encoding`. `+` is a literal character.
|
|
45
|
+
- **New failure reasons:** `invalid-encoding` and `identifying-field`.
|
|
46
|
+
- **Vectors.** The vectors are re-vendored from `1.3.0-rc.2` and verified against upstream's
|
|
47
|
+
`SHA256SUMS`, which is now vendored too. This adds `field-encryption.json` and its
|
|
48
|
+
`decryptFields` operation, and 10 new `encrypted.json` cases.
|
|
49
|
+
|
|
50
|
+
### ⚠️ Breaking changes
|
|
51
|
+
|
|
52
|
+
**Encrypted generation.** The encrypted format follows the 1.3.0 wire format:
|
|
53
|
+
|
|
54
|
+
```
|
|
55
|
+
wemush://v1/encrypted/{id}?e=&iv=&kdf=pbkdf2|raw[&salt=&i=][&sig=]
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
- **Passphrase keys use a random per-label salt.** The fixed `@wemush/wols/crypto/v1` salt is no
|
|
59
|
+
longer used when generating. Every passphrase-encrypted label gets a fresh 16-byte salt, and the
|
|
60
|
+
label records `kdf=pbkdf2`, `salt` and `i`.
|
|
61
|
+
- Labels created by 1.3.0 **cannot** be decrypted by 1.2.x, so upgrade readers before writers.
|
|
62
|
+
- Labels created by 1.2.x have no `kdf`. They fail with reason `legacy-format` unless you pass
|
|
63
|
+
`allowLegacy: true`. Legacy labels are never generated.
|
|
64
|
+
- **Generation iterations default to 600,000 and must be between 100,000 and 10,000,000**
|
|
65
|
+
(previously 100,000 by default, with a minimum of 10,000). `encryptSpecimen` throws outside that
|
|
66
|
+
range. Decoders reject `i` below 10,000 or above 10,000,000.
|
|
67
|
+
- **`EncryptedSpecimen` no longer includes `cryptoVersion`** in generated output. It now has `kdf`,
|
|
68
|
+
`salt` (pbkdf2), `iterations` (pbkdf2), `id`, and `url`. The deprecated `tag` and
|
|
69
|
+
`cryptoVersion` fields will be removed in 2.0.
|
|
70
|
+
- **Field-level encryption** values have the 1.3.0 shape
|
|
71
|
+
`{ encrypted, algorithm, payload, iv, kdf, salt?, iterations? }` instead of
|
|
72
|
+
`{ ..., cryptoVersion }`.
|
|
73
|
+
- **Field-level encryption is strict.** Naming a field the specimen doesn't have (for example a
|
|
74
|
+
typo), naming the same field twice, or naming an inherited property such as `constructor` now
|
|
75
|
+
throws. In 1.2.x such a field was skipped silently and left in cleartext. `@context`, `@type`,
|
|
76
|
+
`id` and `version` can't be encrypted.
|
|
77
|
+
- **`encryptSpecimen(s, { fields: [...] })`** called with a literal `fields` array is now typed as
|
|
78
|
+
returning `FieldEncryptedSpecimen`, which is what it returns at runtime.
|
|
79
|
+
- **`CryptoKey` keys** must be AES-GCM with a length of 256 and allow `encrypt` or `decrypt`. That
|
|
80
|
+
applies to decrypting legacy labels too.
|
|
81
|
+
- **WebCrypto is required.** Node.js 18 has no global `crypto` by default. Use Node.js 20 or later,
|
|
82
|
+
a browser, Deno or Bun. On a runtime without WebCrypto, encryption throws and decryption returns
|
|
83
|
+
`unsupported-environment`.
|
|
84
|
+
|
|
85
|
+
**Validation and creation.**
|
|
86
|
+
|
|
87
|
+
- **The default `idMode` is now `'any'`** (previously `'strict'`): any non-empty suffix after
|
|
88
|
+
`wemush:` is accepted, as the spec requires. This loosens validation. Pass `idMode: 'strict'`,
|
|
89
|
+
`'ulid'` or `'uuid'` to opt into a format check.
|
|
90
|
+
- **`createSpecimen({ type: 'PRIMORDIA' })`** now resolves to `FRUITING`, sets
|
|
91
|
+
`stage: 'PRIMORDIA'` when no stage was supplied, and warns through `console.warn` unless you pass
|
|
92
|
+
`onWarning`.
|
|
93
|
+
- **`registerTypeAlias('PRIMORDIA', …)`** throws for any type other than `FRUITING`.
|
|
94
|
+
- **`isEncrypted`** now narrows to `EncryptedSpecimen | EncryptedField`. Use `isEncryptedSpecimen`
|
|
95
|
+
or `isEncryptedField` to tell them apart.
|
|
96
|
+
|
|
97
|
+
### Added
|
|
98
|
+
|
|
99
|
+
- **Encrypted URLs.** `encryptSpecimen` returns `url`. `decryptSpecimen` accepts the URL or the
|
|
100
|
+
object.
|
|
101
|
+
- **Raw keys (`kdf=raw`).** Pass 32 key bytes (`Uint8Array` or `ArrayBuffer`) or an AES-GCM 256
|
|
102
|
+
`CryptoKey` as `key`. A passphrase string selects `kdf=pbkdf2`.
|
|
103
|
+
- **`encryptFields(specimen, { key, fields })`** returns a typed `FieldEncryptedSpecimen`.
|
|
104
|
+
`decryptFields` returns a validated `Specimen`. `encryptSpecimen`'s `fields` option is
|
|
105
|
+
deprecated.
|
|
106
|
+
- **Typed decryption errors.** A failed decryption carries a `WolsDecryptionError` whose `reason` is a
|
|
107
|
+
`DecryptionFailureReason`, also copied into `details.reason`. The decryption functions return
|
|
108
|
+
`DecryptResult<T>`, which is assignable to `ParseResult<T>`. They never throw.
|
|
109
|
+
- **Decryption hardening.**
|
|
110
|
+
- Every parameter, encoding, the IV length and the key type are checked before PBKDF2 runs, so a
|
|
111
|
+
hostile label costs no key derivation.
|
|
112
|
+
- `maxIterations` (default 10,000,000) bounds the iteration count a label can demand.
|
|
113
|
+
`decryptFields` validates every field first, derives once per distinct salt, and bounds the
|
|
114
|
+
whole document's total.
|
|
115
|
+
- The decrypted `id` must equal `wemush:{id}` from the URL path.
|
|
116
|
+
- Unknown `kdf` values are rejected, as are `salt` or `i` with `kdf=raw`, a `salt` on a legacy
|
|
117
|
+
label, ciphertext shorter than the GCM tag, and plaintext starting with a byte order mark.
|
|
118
|
+
- Padded base64url is accepted.
|
|
119
|
+
- **`toEncryptedUrl` enforces the generator rules.** Every value must be canonical unpadded
|
|
120
|
+
base64url, and the IV must be 12 bytes. It refuses an iteration count outside 100,000 to
|
|
121
|
+
2^32-1, a salt shorter than 16 bytes or byte-equal to the legacy salt, and `salt` with
|
|
122
|
+
`kdf=raw`.
|
|
123
|
+
- **New exports:**
|
|
124
|
+
- Functions: `encryptFields`, `decryptPlaintext` (exact plaintext), `toEncryptedUrl`,
|
|
125
|
+
`parseEncryptedUrl`, `decryptField`, `decryptFields`, `isEncryptedSpecimen`,
|
|
126
|
+
`isEncryptedField`, `derivePassphraseKey`, `base64UrlEncode`, `base64UrlDecode`,
|
|
127
|
+
`resolveTypeAndStage`.
|
|
128
|
+
- Classes: `WolsDecryptionError`.
|
|
129
|
+
- **`strain.transferDepth`** counts every transfer from the original culture.
|
|
130
|
+
- It must be a non-negative integer, and validation warns when it is less than
|
|
131
|
+
`clonalGeneration`.
|
|
132
|
+
- It round-trips through `serializeSpecimen` and `parseSpecimen`.
|
|
133
|
+
- **`createSpecimen` accepts `created`**, as an ISO 8601 string or a `Date`. Invalid values throw.
|
|
134
|
+
When `created` is omitted the field is left out, as before.
|
|
135
|
+
- **`createSpecimen(input, { onWarning })`** receives non-fatal warnings. Without it, warnings go
|
|
136
|
+
to `console.warn`.
|
|
137
|
+
- **`resolveTypeAndStage(type, stage?)`** is the single implementation of the reserved `PRIMORDIA`
|
|
138
|
+
rule. `createSpecimen` and `serializeSpecimen` both use it. `resolveTypeAlias` and
|
|
139
|
+
`mapToWolsType` still map the type only.
|
|
140
|
+
- **Conformance suite.**
|
|
141
|
+
- The WOLS vectors are vendored in `tests/conformance/vectors`, pinned to upstream commit
|
|
142
|
+
`3066545`. Run them with `npm run test:conformance`.
|
|
143
|
+
- `SOURCE.json` records each file's SHA-256 and case count. The runner fails when the vendored
|
|
144
|
+
files or their contents differ from it, when a file has no `MUST` cases, or when a case uses an
|
|
145
|
+
operation it doesn't know.
|
|
146
|
+
- `npm run conformance:update` refreshes the vectors. It discovers upstream files through the
|
|
147
|
+
GitHub API, validates everything before writing anything, and removes stale files.
|
|
148
|
+
- `CHANGELOG.md`.
|
|
149
|
+
|
|
150
|
+
### Changed
|
|
151
|
+
|
|
152
|
+
- **`PRIMORDIA` supplied as a type** is never serialized as a `type`. `serializeSpecimen` resolves
|
|
153
|
+
it on hand-built objects too.
|
|
154
|
+
- `isValidSpecimenId(id, strict?)` accepts any non-empty suffix by default, matching `idMode`.
|
|
155
|
+
- **`strain.clonalGeneration` accepts `0`** for the original culture. It previously required
|
|
156
|
+
`>= 1`, which contradicted the spec.
|
|
157
|
+
- **`strain.generation` accepts `P<n>`** (for example `P1`).
|
|
158
|
+
- `WOLS_VERSION` is `1.3.0`, so new specimens are stamped with `version: "1.3.0"`.
|
|
159
|
+
- Growth-stage validation messages list all seven stages.
|
|
160
|
+
|
|
161
|
+
### Fixed
|
|
162
|
+
|
|
163
|
+
- **`require('@wemush/wols')` works** ([#9](https://github.com/wemush/specimen-labels-js/pull/9)).
|
|
164
|
+
It was broken in 1.2.0 because `dist/cjs` was loaded as ESM. CommonJS consumers now also get
|
|
165
|
+
CommonJS type declarations.
|
|
166
|
+
|
|
167
|
+
### Unchanged
|
|
168
|
+
|
|
169
|
+
- The compact URL format. Its parameter and species-code registries aren't settled yet.
|
|
170
|
+
- `sig` is preserved when parsing and generating encrypted URLs, but it isn't verified.
|
package/README.md
CHANGED
|
@@ -12,7 +12,8 @@ Official TypeScript library for **WOLS** (WeMush Open Labeling Standard) - the o
|
|
|
12
12
|
- 📜 **Parse** - Parse JSON/QR data back to typed specimens
|
|
13
13
|
- 📱 **QR Codes** - Generate QR codes for specimens (PNG, SVG, Data URL)
|
|
14
14
|
- 🔗 **Compact URLs** - Create short URLs for space-constrained labels
|
|
15
|
-
- 🔐 **Encryption** - AES-256-GCM
|
|
15
|
+
- 🔐 **Encryption** - WOLS 1.3.0 encrypted format (AES-256-GCM, per-label PBKDF2 salt or raw keys)
|
|
16
|
+
- 🧪 **Conformance** - Passes every `MUST` vector in the WOLS 1.3.0 conformance suite
|
|
16
17
|
|
|
17
18
|
## Installation
|
|
18
19
|
|
|
@@ -51,7 +52,7 @@ console.log(specimen['@context']); // "https://wemush.com/wols/v1"
|
|
|
51
52
|
|
|
52
53
|
## Core Module API
|
|
53
54
|
|
|
54
|
-
### `createSpecimen(input: SpecimenInput): Specimen`
|
|
55
|
+
### `createSpecimen(input: SpecimenInput, options?): Specimen`
|
|
55
56
|
|
|
56
57
|
Creates a new specimen with auto-generated ID and JSON-LD fields.
|
|
57
58
|
|
|
@@ -59,8 +60,9 @@ Creates a new specimen with auto-generated ID and JSON-LD fields.
|
|
|
59
60
|
const specimen = createSpecimen({
|
|
60
61
|
type: 'SPAWN',
|
|
61
62
|
species: 'Hericium erinaceus',
|
|
62
|
-
strain: { name: "Lion's Mane", generation: 'F2' },
|
|
63
|
+
strain: { name: "Lion's Mane", generation: 'F2', clonalGeneration: 3, transferDepth: 4 },
|
|
63
64
|
stage: 'INOCULATION',
|
|
65
|
+
created: '2025-12-16T10:30:00Z',
|
|
64
66
|
batch: 'BATCH-2024-001',
|
|
65
67
|
organization: 'org_123',
|
|
66
68
|
custom: { substrate: 'hardwood' },
|
|
@@ -69,15 +71,53 @@ const specimen = createSpecimen({
|
|
|
69
71
|
|
|
70
72
|
**Parameters:**
|
|
71
73
|
|
|
72
|
-
- `type` - Specimen type: `'CULTURE'`, `'SPAWN'`, `'SUBSTRATE'`, `'FRUITING'`, `'HARVEST'`
|
|
74
|
+
- `type` - Specimen type: `'CULTURE'`, `'SPAWN'`, `'SUBSTRATE'`, `'FRUITING'`, `'HARVEST'`, or an alias such as `'LC'`, `'GRAIN_SPAWN'`, `'BAG'`, `'PINNING'` (case-insensitive)
|
|
73
75
|
- `species` - Scientific species name
|
|
74
76
|
- `strain` - String (auto-expands to `{ name: string }`) or full `Strain` object
|
|
75
|
-
- `stage` - Growth stage: `'INOCULATION'`, `'COLONIZATION'`, `'FRUITING'`, `'HARVEST'`
|
|
77
|
+
- `stage` - Growth stage: `'INOCULATION'`, `'INCUBATION'`, `'COLONIZATION'`, `'PRIMORDIA'`, `'FRUITING'`, `'SENESCENCE'`, `'HARVEST'`
|
|
78
|
+
- `created` - Creation timestamp, an ISO 8601 string or a `Date` (omitted when not supplied; invalid values throw)
|
|
76
79
|
- `batch` - Batch identifier
|
|
77
80
|
- `organization` - Organization ID
|
|
78
81
|
- `creator` - Creator user ID
|
|
79
82
|
- `custom` - Custom vendor-specific fields
|
|
80
83
|
|
|
84
|
+
**Options:**
|
|
85
|
+
|
|
86
|
+
- `onWarning(warning)` - Receives non-fatal warnings. If omitted, warnings go to `console.warn`.
|
|
87
|
+
|
|
88
|
+
#### `PRIMORDIA` as a type
|
|
89
|
+
|
|
90
|
+
`PRIMORDIA` is a growth stage, not a specimen type. Because people often supply it as a type,
|
|
91
|
+
WOLS 1.3.0 makes it a reserved alias. When `type` is `PRIMORDIA` (in any case), the library:
|
|
92
|
+
|
|
93
|
+
- resolves the type to `FRUITING`
|
|
94
|
+
- sets `stage: 'PRIMORDIA'` if no stage was supplied, and never overrides a supplied stage
|
|
95
|
+
- emits a `PRIMORDIA_AS_TYPE` warning that recommends `type: 'FRUITING', stage: 'PRIMORDIA'`
|
|
96
|
+
- never serializes `PRIMORDIA` as a `type`. `serializeSpecimen` applies the same resolution to hand-built objects.
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const s = createSpecimen(
|
|
100
|
+
{ type: 'primordia', species: 'Pleurotus ostreatus' },
|
|
101
|
+
{ onWarning: w => log.info(w.message) }
|
|
102
|
+
);
|
|
103
|
+
// s.type === 'FRUITING', s.stage === 'PRIMORDIA'
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
`validateSpecimen` still rejects `type: "PRIMORDIA"` in a serialized label.
|
|
107
|
+
|
|
108
|
+
#### Generation, subculture, and transfer depth
|
|
109
|
+
|
|
110
|
+
`strain` carries three independent counters (WOLS 1.3.0):
|
|
111
|
+
|
|
112
|
+
| Field | Meaning | Original culture |
|
|
113
|
+
| ------------------ | ---------------------------------------------------------- | ---------------- |
|
|
114
|
+
| `generation` | Filial generation (`P`, `F1`, `G2`, `3`, …) | `P` |
|
|
115
|
+
| `clonalGeneration` | Culture-to-culture subcultures | `0` |
|
|
116
|
+
| `transferDepth` | Transfers of any kind (subculture, to grain, to substrate) | `0` |
|
|
117
|
+
|
|
118
|
+
Both counters must be non-negative integers. Validation warns when `transferDepth` is less
|
|
119
|
+
than `clonalGeneration`.
|
|
120
|
+
|
|
81
121
|
### `serializeSpecimen(specimen: Specimen): string`
|
|
82
122
|
|
|
83
123
|
Serializes a specimen to a JSON-LD string, suitable for QR encoding.
|
|
@@ -119,6 +159,11 @@ if (!result.valid) {
|
|
|
119
159
|
|
|
120
160
|
- `allowUnknownFields` - Allow unknown fields in custom namespace
|
|
121
161
|
- `level` - Validation strictness: `'strict'` or `'lenient'`
|
|
162
|
+
- `idMode` - ID check. Every mode requires the `wemush:` prefix.
|
|
163
|
+
- `'any'` (default): any non-empty suffix, as WOLS requires. CUIDs, ULIDs, UUIDs and human-readable IDs are all accepted.
|
|
164
|
+
- `'strict'` (opt-in): the suffix must be a CUID-style `[a-z0-9]+`.
|
|
165
|
+
- `'ulid'` / `'uuid'` (opt-in): the suffix must be a ULID or a UUID.
|
|
166
|
+
- `customIdValidator` - Your own `(id) => boolean` check, which takes precedence over `idMode`
|
|
122
167
|
|
|
123
168
|
## QR Code Module API
|
|
124
169
|
|
|
@@ -189,57 +234,148 @@ if (result.success) {
|
|
|
189
234
|
|
|
190
235
|
## Encryption Module API
|
|
191
236
|
|
|
192
|
-
|
|
237
|
+
Implements the WOLS 1.3.0 encrypted format with WebCrypto, so it works in Node.js 20+, browsers,
|
|
238
|
+
Deno and Bun.
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
wemush://v1/encrypted/{id}?e={ciphertext}&iv={iv}&kdf=pbkdf2|raw[&salt={salt}&i={iterations}][&sig={signature}]
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
- AES-256-GCM. `e` is the ciphertext with the 16-byte tag appended. `iv` is a fresh random 12-byte IV for every encryption.
|
|
245
|
+
- Every binary value is base64url without padding. Decoders also accept padded input.
|
|
246
|
+
- `{id}` is the specimen ID without the `wemush:` prefix, percent-encoded as one URL path segment (`encodeURIComponent`). Decoders percent-decode it as UTF-8 before the `id` check, keep `+` as a literal character, and reject malformed escapes or invalid UTF-8. It is cleartext, so don't put sensitive data in it.
|
|
247
|
+
- Passphrases are normalized to Unicode NFC before PBKDF2, so a passphrase typed in composed or decomposed form (for example `ü` as one code point, or `u` plus a combining mark) gives the same key.
|
|
193
248
|
|
|
194
249
|
```typescript
|
|
195
|
-
import { encryptSpecimen, decryptSpecimen
|
|
250
|
+
import { encryptSpecimen, decryptSpecimen } from '@wemush/wols';
|
|
196
251
|
// Or import from submodule:
|
|
197
252
|
// import { encryptSpecimen } from '@wemush/wols/crypto';
|
|
198
253
|
```
|
|
199
254
|
|
|
200
255
|
### `encryptSpecimen(specimen, options): Promise<EncryptedSpecimen>`
|
|
201
256
|
|
|
202
|
-
|
|
257
|
+
Encrypts the whole label. The kind of `key` you pass selects the key derivation:
|
|
258
|
+
|
|
259
|
+
| `key` | `kdf` | Behavior |
|
|
260
|
+
| ------------------------------------------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
|
|
261
|
+
| passphrase `string` | `pbkdf2` | PBKDF2-HMAC-SHA256 over the NFC-normalized passphrase, a fresh random 16-byte salt per label, 600,000 iterations by default |
|
|
262
|
+
| 32-byte `Uint8Array` / `ArrayBuffer`, or an AES-GCM 256 `CryptoKey` | `raw` | Key shared out of band. No `salt` or `i`. |
|
|
203
263
|
|
|
204
264
|
```typescript
|
|
205
|
-
const
|
|
206
|
-
|
|
207
|
-
|
|
265
|
+
const enc = await encryptSpecimen(specimen, { key: 'correct horse battery staple' });
|
|
266
|
+
enc.url;
|
|
267
|
+
// "wemush://v1/encrypted/clx1a2b3c4?e=...&iv=...&kdf=pbkdf2&salt=...&i=600000"
|
|
208
268
|
|
|
209
|
-
|
|
210
|
-
//
|
|
269
|
+
const raw = await encryptSpecimen(specimen, { key: crypto.getRandomValues(new Uint8Array(32)) });
|
|
270
|
+
raw.url; // "wemush://v1/encrypted/clx1a2b3c4?e=...&iv=...&kdf=raw"
|
|
211
271
|
```
|
|
212
272
|
|
|
213
|
-
|
|
273
|
+
The result also carries `payload`, `iv`, `kdf`, `salt`, `iterations` and `id`. Put `url` in the QR code.
|
|
274
|
+
|
|
275
|
+
**Options:**
|
|
276
|
+
|
|
277
|
+
- `key` - A passphrase (`kdf=pbkdf2`), or raw key bytes or a `CryptoKey` (`kdf=raw`)
|
|
278
|
+
- `iterations` - PBKDF2 iterations. Passphrase keys only. The default is 600,000, and anything
|
|
279
|
+
from 100,000 to 10,000,000 is allowed.
|
|
280
|
+
- `fields` - **Deprecated.** Use `encryptFields` instead.
|
|
281
|
+
|
|
282
|
+
### `encryptFields(specimen, { key, fields }): Promise<FieldEncryptedSpecimen>`
|
|
214
283
|
|
|
215
|
-
|
|
284
|
+
Field-level encryption. Each named field's value is replaced in place with
|
|
285
|
+
`{ encrypted: true, algorithm: "AES-256-GCM", payload, iv, kdf, salt?, iterations? }`, and the rest
|
|
286
|
+
of the label stays in cleartext. Every name in `fields` must be a field the specimen actually has.
|
|
287
|
+
A misspelled or missing field throws instead of being left unprotected. `@context`, `@type`, `id`,
|
|
288
|
+
`version`, `type` and `species` identify the label, so they can't be encrypted. The result is typed
|
|
289
|
+
precisely: the named fields are `EncryptedField`, and every other field keeps its cleartext type.
|
|
216
290
|
|
|
217
291
|
```typescript
|
|
218
|
-
const
|
|
219
|
-
|
|
220
|
-
}
|
|
292
|
+
const partial = await encryptFields(specimen, { key: passphrase, fields: ['strain'] });
|
|
293
|
+
partial.species; // cleartext
|
|
294
|
+
partial.strain; // { encrypted: true, kdf: 'pbkdf2', ... }
|
|
221
295
|
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
} else {
|
|
225
|
-
console.error('Decryption failed:', result.error.message);
|
|
226
|
-
}
|
|
296
|
+
const restored = await decryptFields(partial, { key: passphrase });
|
|
297
|
+
if (restored.success) restored.data.strain?.name; // a validated Specimen
|
|
227
298
|
```
|
|
228
299
|
|
|
229
|
-
|
|
300
|
+
Each value is bound to its label and field. The AES-GCM additional data is the UTF-8 encoding of
|
|
301
|
+
the label's full `id`, a line feed, and the field name, for example `"wemush:clx1a2b3c4\nstrain"`.
|
|
302
|
+
Every field gets its own random IV. A value moved to another field, copied from another label, or
|
|
303
|
+
left behind after the `id` changes fails with `authentication-failed`.
|
|
304
|
+
|
|
305
|
+
`decryptFields` refuses a label in which an identifying field is encrypted (`identifying-field`).
|
|
306
|
+
It never substitutes a decrypted value for one. Before deriving any key, it adds up the iterations
|
|
307
|
+
of each distinct salt and iteration-count pair, and refuses the label when the total exceeds
|
|
308
|
+
`maxIterations`.
|
|
309
|
+
|
|
310
|
+
To decrypt one value, pass its binding: `decryptField(partial.strain, { key }, { id: partial.id, field: 'strain' })`.
|
|
230
311
|
|
|
231
|
-
|
|
312
|
+
### `decryptSpecimen(input, options): Promise<DecryptResult<Specimen>>`
|
|
313
|
+
|
|
314
|
+
`input` can be an encrypted URL or an `EncryptedSpecimen` object. Use `decryptPlaintext` to get the
|
|
315
|
+
exact decrypted JSON string instead. Decryption never throws. A failure carries a
|
|
316
|
+
`WolsDecryptionError` with a typed `reason`:
|
|
232
317
|
|
|
233
318
|
```typescript
|
|
234
|
-
|
|
235
|
-
|
|
319
|
+
const result = await decryptSpecimen(url, { key: 'correct horse battery staple' });
|
|
320
|
+
|
|
321
|
+
if (result.success) {
|
|
322
|
+
console.log(result.data.species);
|
|
323
|
+
} else if (result.error.reason === 'legacy-format') {
|
|
324
|
+
// a pre-1.3.0 label: see "Legacy labels" below
|
|
325
|
+
} else {
|
|
326
|
+
console.error(result.error.reason, result.error.message);
|
|
236
327
|
}
|
|
237
328
|
```
|
|
238
329
|
|
|
239
|
-
**
|
|
330
|
+
**Options:**
|
|
240
331
|
|
|
241
|
-
- `key` -
|
|
242
|
-
- `
|
|
332
|
+
- `key` - The passphrase or raw key
|
|
333
|
+
- `allowLegacy` - Decrypt pre-1.3.0 labels (see below)
|
|
334
|
+
- `maxIterations` - The largest PBKDF2 iteration count accepted from a label. The default and the
|
|
335
|
+
maximum are both 10,000,000. You can set it lower, never higher. The count comes from the label,
|
|
336
|
+
so this caps how much CPU a hostile QR code can cost. A server that decodes untrusted labels
|
|
337
|
+
should set it close to the count it generates with. `decryptFields` applies it to the whole
|
|
338
|
+
label: fields that share a salt share one key derivation, and the total across distinct salts
|
|
339
|
+
must stay under the limit.
|
|
340
|
+
|
|
341
|
+
Every cheap check runs before key derivation, so a malformed label costs no PBKDF2 work. The
|
|
342
|
+
failure reasons are:
|
|
343
|
+
|
|
344
|
+
| `reason` | When |
|
|
345
|
+
| --- | --- |
|
|
346
|
+
| `id-mismatch` | The decrypted specimen's `id` isn't `wemush:{id}` from the URL path |
|
|
347
|
+
| `authentication-failed` | The key is wrong, or the ciphertext was tampered with |
|
|
348
|
+
| `insufficient-iterations` / `excessive-iterations` | `i` is below 10,000, or above `maxIterations` (alone, or in total for field-level) |
|
|
349
|
+
| `missing-parameter` | `e` or `iv` is missing, or `kdf=pbkdf2` lacks `salt` or `i` |
|
|
350
|
+
| `unexpected-parameter` | `kdf=raw` has `salt` or `i`, or a legacy label has `salt` |
|
|
351
|
+
| `invalid-parameter` | Bad base64url, `i` that isn't plain decimal digits, an IV that isn't 12 bytes, ciphertext shorter than the tag, a duplicate parameter |
|
|
352
|
+
| `invalid-encoding` | The ID in the URL path has a malformed percent-escape or isn't valid UTF-8 |
|
|
353
|
+
| `identifying-field` | A field-level label has an encrypted `@context`, `@type`, `id`, `version`, `type` or `species` |
|
|
354
|
+
| `unsupported-kdf` | `kdf` is something other than `pbkdf2` or `raw` |
|
|
355
|
+
| `invalid-key` | The key doesn't fit the label, for example a passphrase for a `raw` label |
|
|
356
|
+
| `legacy-format` | No `kdf`: a pre-1.3.0 label, and `allowLegacy` wasn't set |
|
|
357
|
+
| `invalid-plaintext` | The decrypted bytes aren't UTF-8 JSON (or start with a BOM), or aren't a valid specimen |
|
|
358
|
+
| `invalid-url` | Not a `wemush://v1/encrypted/...` URL |
|
|
359
|
+
| `unsupported-environment` | The runtime has no WebCrypto |
|
|
360
|
+
|
|
361
|
+
The `id` check applies to the URL form and to objects that carry an `id`. An object without `id`
|
|
362
|
+
isn't bound to any ID, so prefer URLs for input you don't trust.
|
|
363
|
+
|
|
364
|
+
After a label authenticates, error messages and `details` can contain decrypted content, such as
|
|
365
|
+
the mismatched `id` or the specimen validation errors. Don't forward them to untrusted parties.
|
|
366
|
+
|
|
367
|
+
**Legacy labels:** payloads from before 1.3.0 have no `kdf`. They were encrypted with a fixed
|
|
368
|
+
library-wide salt, and they fail with `legacy-format` by default. Pass `{ allowLegacy: true }` only
|
|
369
|
+
when you are decrypting labels you know are old, for example during a migration. Re-encrypt them
|
|
370
|
+
with `encryptSpecimen`. Legacy labels are never generated.
|
|
371
|
+
|
|
372
|
+
### Other helpers
|
|
373
|
+
|
|
374
|
+
- `toEncryptedUrl(encrypted)` / `parseEncryptedUrl(url)` - convert between the URL and the object form. `toEncryptedUrl` refuses objects that would make a non-conformant label: values that aren't canonical unpadded base64url, an IV that isn't 12 bytes, fewer than 100,000 iterations, a salt that is short or byte-equal to the legacy salt, or `salt` with `kdf=raw`.
|
|
375
|
+
- `decryptField(field, options)` - decrypt a single encrypted field value
|
|
376
|
+
- `isEncryptedSpecimen(value)` / `isEncryptedField(value)` - tell a whole-label encryption from a field-level one. `isEncrypted(value)` matches either.
|
|
377
|
+
- `derivePassphraseKey(passphrase, salt, iterations)` - raw PBKDF2 output, for debugging interoperability. It doesn't apply the label rules.
|
|
378
|
+
- `base64UrlEncode(bytes)` / `base64UrlDecode(str)`
|
|
243
379
|
|
|
244
380
|
## Types
|
|
245
381
|
|
|
@@ -257,7 +393,13 @@ import type {
|
|
|
257
393
|
ValidationResult,
|
|
258
394
|
ValidationError,
|
|
259
395
|
EncryptedSpecimen,
|
|
396
|
+
EncryptedField,
|
|
397
|
+
FieldEncryptedSpecimen,
|
|
260
398
|
EncryptionOptions,
|
|
399
|
+
FieldEncryptionOptions,
|
|
400
|
+
DecryptionOptions,
|
|
401
|
+
DecryptionFailureReason,
|
|
402
|
+
DecryptResult,
|
|
261
403
|
QRCodeOptions,
|
|
262
404
|
} from '@wemush/wols';
|
|
263
405
|
```
|
|
@@ -287,7 +429,38 @@ if (!result.success) {
|
|
|
287
429
|
|
|
288
430
|
## WOLS Specification
|
|
289
431
|
|
|
290
|
-
This library implements **WOLS v1.
|
|
432
|
+
This library implements **WOLS v1.3.0**. For the full specification, see the
|
|
433
|
+
[WOLS Specification](https://github.com/wemush/open-standard/blob/main/SPECIFICATION.md).
|
|
434
|
+
|
|
435
|
+
### Conformance
|
|
436
|
+
|
|
437
|
+
The shared [WOLS conformance vectors](https://github.com/wemush/open-standard/tree/main/conformance)
|
|
438
|
+
are vendored in `tests/conformance/vectors` and run with `npm test` (or `npm run test:conformance`).
|
|
439
|
+
Every `MUST` case passes. `SHOULD` cases are reported but don't fail the build. See
|
|
440
|
+
[`tests/conformance/README.md`](tests/conformance/README.md) for how to refresh the vectors.
|
|
441
|
+
|
|
442
|
+
### Upgrading from 1.2
|
|
443
|
+
|
|
444
|
+
See [CHANGELOG.md](CHANGELOG.md). Every change that can break a 1.2.x caller:
|
|
445
|
+
|
|
446
|
+
- **Encrypted generation changed.** Passphrase encryption now uses a random per-label salt and
|
|
447
|
+
records `kdf`, `salt` and `i`. Labels made by 1.3.0 can't be read by 1.2.x, so upgrade readers
|
|
448
|
+
before writers. 1.2.x labels fail with `legacy-format` unless you pass `allowLegacy: true`.
|
|
449
|
+
- Passphrase encryption defaults to 600,000 iterations and allows 100,000 to 10,000,000. It used
|
|
450
|
+
to default to 100,000, with a minimum of 10,000. Passphrases are now normalized to NFC.
|
|
451
|
+
- A `CryptoKey` must now be AES-GCM with a length of 256 and allow `encrypt` or `decrypt`. That
|
|
452
|
+
applies to legacy labels too.
|
|
453
|
+
- Field-level encryption (`fields`) throws for a field the specimen doesn't have. It used to skip
|
|
454
|
+
the field silently, leaving it in cleartext. Field values use the 1.3.0 shape (`kdf`, `salt`,
|
|
455
|
+
`iterations` instead of `cryptoVersion`).
|
|
456
|
+
- The default `idMode` is now `'any'`. Pass `idMode: 'strict'` to keep the old CUID-style check.
|
|
457
|
+
This loosens validation, so if `validateSpecimen` gates untrusted input, decide which mode you
|
|
458
|
+
want.
|
|
459
|
+
- `createSpecimen({ type: 'PRIMORDIA' })` now sets `stage: 'PRIMORDIA'` and warns through
|
|
460
|
+
`console.warn` unless you pass `onWarning`. `registerTypeAlias('PRIMORDIA', …)` with anything other
|
|
461
|
+
than `FRUITING` throws.
|
|
462
|
+
- `isEncrypted` now narrows to `EncryptedSpecimen | EncryptedField`.
|
|
463
|
+
- The encryption functions need a global WebCrypto, which means Node.js 20 or later.
|
|
291
464
|
|
|
292
465
|
## License
|
|
293
466
|
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compact URL Format Functions
|
|
3
|
+
*
|
|
4
|
+
* This module provides functions to generate and parse compact URL format
|
|
5
|
+
* for WOLS specimens. The compact format is designed for small QR codes
|
|
6
|
+
* (petri dishes, culture tubes) where space is limited.
|
|
7
|
+
*
|
|
8
|
+
* Format: wemush://v1/{id}?s={species_code}&st={stage}&t={timestamp}
|
|
9
|
+
*
|
|
10
|
+
* @module compact-url
|
|
11
|
+
*/
|
|
12
|
+
import type { Specimen, ParseResult, SpecimenRef } from './types.js';
|
|
13
|
+
/**
|
|
14
|
+
* Standard species codes from the WOLS specification
|
|
15
|
+
*
|
|
16
|
+
* @see https://github.com/wemush/open-standard for additional codes
|
|
17
|
+
*/
|
|
18
|
+
export declare const SPECIES_CODES: Record<string, string>;
|
|
19
|
+
/**
|
|
20
|
+
* Get the full species name from a species code
|
|
21
|
+
*
|
|
22
|
+
* @param code - Species code (e.g., "POSTR")
|
|
23
|
+
* @returns Species name or undefined if code is unknown
|
|
24
|
+
*
|
|
25
|
+
* @example
|
|
26
|
+
* ```typescript
|
|
27
|
+
* getSpeciesFromCode('POSTR') // Returns "Pleurotus ostreatus"
|
|
28
|
+
* getSpeciesFromCode('XXXXX') // Returns undefined
|
|
29
|
+
* ```
|
|
30
|
+
*/
|
|
31
|
+
export declare function getSpeciesFromCode(code: string): string | undefined;
|
|
32
|
+
/**
|
|
33
|
+
* Get the species code from a species name
|
|
34
|
+
*
|
|
35
|
+
* @param species - Full species name (e.g., "Pleurotus ostreatus")
|
|
36
|
+
* @returns Species code or undefined if species is not registered
|
|
37
|
+
*
|
|
38
|
+
* @example
|
|
39
|
+
* ```typescript
|
|
40
|
+
* getCodeFromSpecies('Pleurotus ostreatus') // Returns "POSTR"
|
|
41
|
+
* getCodeFromSpecies('Unknown species') // Returns undefined
|
|
42
|
+
* ```
|
|
43
|
+
*/
|
|
44
|
+
export declare function getCodeFromSpecies(species: string): string | undefined;
|
|
45
|
+
/**
|
|
46
|
+
* Register a custom species code
|
|
47
|
+
*
|
|
48
|
+
* Use this to add species codes that are not in the standard WOLS list.
|
|
49
|
+
*
|
|
50
|
+
* @param code - Species code (uppercase, 5 characters recommended)
|
|
51
|
+
* @param species - Full species name
|
|
52
|
+
*
|
|
53
|
+
* @example
|
|
54
|
+
* ```typescript
|
|
55
|
+
* registerSpeciesCode('CCOMA', 'Coprinus comatus');
|
|
56
|
+
* getSpeciesFromCode('CCOMA') // Returns "Coprinus comatus"
|
|
57
|
+
* ```
|
|
58
|
+
*/
|
|
59
|
+
export declare function registerSpeciesCode(code: string, species: string): void;
|
|
60
|
+
/**
|
|
61
|
+
* Generate a compact URL from a specimen
|
|
62
|
+
*
|
|
63
|
+
* The compact URL format is designed for small QR codes:
|
|
64
|
+
* ```
|
|
65
|
+
* wemush://v1/{id}?s={species_code}&st={stage}&t={timestamp}
|
|
66
|
+
* ```
|
|
67
|
+
*
|
|
68
|
+
* @param specimen - The specimen to encode
|
|
69
|
+
* @returns Compact URL string
|
|
70
|
+
*
|
|
71
|
+
* @example
|
|
72
|
+
* ```typescript
|
|
73
|
+
* const specimen = createSpecimen({
|
|
74
|
+
* type: 'CULTURE',
|
|
75
|
+
* species: 'Pleurotus ostreatus',
|
|
76
|
+
* stage: 'COLONIZATION'
|
|
77
|
+
* });
|
|
78
|
+
*
|
|
79
|
+
* const url = toCompactUrl(specimen);
|
|
80
|
+
* // wemush://v1/clx1a2b3c4?s=POSTR&st=COLONIZATION&ty=CULTURE&t=1734307200
|
|
81
|
+
* ```
|
|
82
|
+
*/
|
|
83
|
+
export declare function toCompactUrl(specimen: Specimen): string;
|
|
84
|
+
/**
|
|
85
|
+
* Parse a compact URL into a SpecimenRef
|
|
86
|
+
*
|
|
87
|
+
* Returns a reference object containing the minimal information
|
|
88
|
+
* encoded in the compact URL. Use the id to fetch full specimen
|
|
89
|
+
* data from the API if needed.
|
|
90
|
+
*
|
|
91
|
+
* @param url - Compact URL to parse (wemush://v1/...)
|
|
92
|
+
* @returns ParseResult with SpecimenRef on success, error on failure
|
|
93
|
+
*
|
|
94
|
+
* @example
|
|
95
|
+
* ```typescript
|
|
96
|
+
* const result = parseCompactUrl('wemush://v1/clx1a2b3c4?s=POSTR&st=COLONIZATION');
|
|
97
|
+
*
|
|
98
|
+
* if (result.success) {
|
|
99
|
+
* console.log(result.data.species); // "Pleurotus ostreatus"
|
|
100
|
+
* }
|
|
101
|
+
* ```
|
|
102
|
+
*/
|
|
103
|
+
export declare function parseCompactUrl(url: string): ParseResult<SpecimenRef>;
|
|
104
|
+
/**
|
|
105
|
+
* Parse a compact URL, throwing an error on failure.
|
|
106
|
+
*
|
|
107
|
+
* This is a convenience wrapper around parseCompactUrl() for cases
|
|
108
|
+
* where you prefer exceptions over result types.
|
|
109
|
+
*
|
|
110
|
+
* @param url - Compact URL to parse (wemush://v1/...)
|
|
111
|
+
* @returns SpecimenRef on success
|
|
112
|
+
* @throws WolsParseError on parse failure
|
|
113
|
+
*
|
|
114
|
+
* @example
|
|
115
|
+
* ```typescript
|
|
116
|
+
* try {
|
|
117
|
+
* const ref = parseCompactUrlOrThrow('wemush://v1/clx1a2b3c4?s=POSTR');
|
|
118
|
+
* console.log(ref.species); // "Pleurotus ostreatus"
|
|
119
|
+
* } catch (err) {
|
|
120
|
+
* console.error('Failed to parse:', err.message);
|
|
121
|
+
* }
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @since 1.2.0
|
|
125
|
+
*/
|
|
126
|
+
export declare function parseCompactUrlOrThrow(url: string): SpecimenRef;
|
|
127
|
+
/**
|
|
128
|
+
* Parse a compact URL, returning null on failure.
|
|
129
|
+
*
|
|
130
|
+
* This is a convenience wrapper around parseCompactUrl() for cases
|
|
131
|
+
* where you want to handle failures with nullish coalescing.
|
|
132
|
+
*
|
|
133
|
+
* @param url - Compact URL to parse (wemush://v1/...)
|
|
134
|
+
* @returns SpecimenRef on success, null on failure
|
|
135
|
+
*
|
|
136
|
+
* @example
|
|
137
|
+
* ```typescript
|
|
138
|
+
* const ref = parseCompactUrlOrNull('wemush://v1/clx1a2b3c4?s=POSTR');
|
|
139
|
+
* if (ref !== null) {
|
|
140
|
+
* console.log(ref.species);
|
|
141
|
+
* }
|
|
142
|
+
*
|
|
143
|
+
* // Or with nullish coalescing:
|
|
144
|
+
* const species = parseCompactUrlOrNull(url)?.species ?? 'Unknown';
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* @since 1.2.0
|
|
148
|
+
*/
|
|
149
|
+
export declare function parseCompactUrlOrNull(url: string): SpecimenRef | null;
|
|
150
|
+
//# sourceMappingURL=compact-url.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compact-url.d.ts","sourceRoot":"","sources":["../../src/compact-url.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;GAUG;AAGH,OAAO,KAAK,EAAE,QAAQ,EAA6B,WAAW,EAAE,WAAW,EAAE,MAAM,YAAY,CAAC;AAEhG;;;;GAIG;AACH,eAAO,MAAM,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAWhD,CAAC;AAYF;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEnE;AAED;;;;;;;;;;;GAWG;AACH,wBAAgB,kBAAkB,CAAC,OAAO,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS,CAEtE;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI,CASvE;AAED;;;;;;;;;;;;;;;;;;;;;;GAsBG;AACH,wBAAgB,YAAY,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAsDvD;AAED;;;;;;;;;;;;;;;;;;GAkBG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAAC,WAAW,CAAC,CA6GrE;AAMD;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,sBAAsB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,CAM/D;AAED;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,MAAM,GAAG,WAAW,GAAG,IAAI,CAOrE"}
|