wyvrnpm 2.12.3 → 2.13.12

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,301 @@
1
+ 'use strict';
2
+
3
+ const { canonicalize } = require('./canonical');
4
+
5
+ /**
6
+ * Builder + validator for the wyvrnpm artefact attestation. Two shapes
7
+ * coexist on the registry:
8
+ *
9
+ * v1 — fat-zip publish (today's default).
10
+ * Binds contentSha256 (the single `wyvrn.zip`) + manifestSha256.
11
+ *
12
+ * v2 — per-config slice publish (recipe `build.publishPerConfig: true`).
13
+ * Binds artefactSha256 (a `{ config → sha256 }` map covering every
14
+ * `wyvrn-<Config>.zip` slice) + manifestSha256. The signed canonical
15
+ * body covers the whole map, so a tampered or omitted slice changes
16
+ * the signed bytes and the signature fails — even if the slice the
17
+ * consumer is asking for is unchanged.
18
+ *
19
+ * Both shapes share name/version/profileHash/manifestSha256/publishedAt/
20
+ * publisherKeyId/uploadedBy. A v1 attestation is byte-identical to what
21
+ * pre-2.13.x publishers produced, so existing trust anchors and signed
22
+ * artefacts on the registry stay verifiable without rotation.
23
+ */
24
+
25
+ const ATTESTATION_V1 = 1;
26
+ const ATTESTATION_V2 = 2;
27
+ const SUPPORTED_ATTESTATION_VERSIONS = new Set([ATTESTATION_V1, ATTESTATION_V2]);
28
+
29
+ // Legacy alias — pre-v2 callers imported `ATTESTATION_VERSION` literally.
30
+ // Keep it pointing at v1 so they keep emitting v1 unless they opt in.
31
+ const ATTESTATION_VERSION = ATTESTATION_V1;
32
+
33
+ const SHA256_HEX_RE = /^[0-9a-f]{64}$/;
34
+ const PROFILE_HASH_RE = /^[0-9a-f]{16}$/;
35
+ // keyId lives in keys.json + signature envelope; keep it ASCII + slug-shaped
36
+ // so it round-trips cleanly through filesystems and config files.
37
+ const KEY_ID_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
38
+ const ISO8601_RE = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?Z$/;
39
+ // CMake config names are PascalCase, ASCII, no spaces. The slice classifier
40
+ // only emits canonical configs (Debug / Release / RelWithDebInfo /
41
+ // MinSizeRel) but the regex stays generous so a future custom-config
42
+ // extension doesn't require a schema bump.
43
+ const CONFIG_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
44
+
45
+ const REQUIRED_V1 = [
46
+ 'wyvrnAttestationVersion', 'name', 'version', 'profileHash',
47
+ 'contentSha256', 'manifestSha256', 'publishedAt', 'publisherKeyId',
48
+ ];
49
+ const REQUIRED_V2 = [
50
+ 'wyvrnAttestationVersion', 'name', 'version', 'profileHash',
51
+ 'artefactSha256', 'manifestSha256', 'publishedAt', 'publisherKeyId',
52
+ ];
53
+
54
+ /**
55
+ * Build a fully-validated attestation. Dispatch:
56
+ * - explicit `wyvrnAttestationVersion`: honoured.
57
+ * - else: v2 if `artefactSha256` is supplied; v1 otherwise.
58
+ *
59
+ * Both v1 and v2 inputs are mutually exclusive — supplying both
60
+ * `contentSha256` and `artefactSha256` is incoherent and throws.
61
+ */
62
+ function buildAttestation(opts) {
63
+ if (opts == null || typeof opts !== 'object') {
64
+ throw new Error('attestation: opts must be an object');
65
+ }
66
+ const v = opts.wyvrnAttestationVersion;
67
+ if (v !== undefined && v !== null && !SUPPORTED_ATTESTATION_VERSIONS.has(v)) {
68
+ throw new Error(
69
+ `attestation: unsupported wyvrnAttestationVersion ${v}` +
70
+ ` (this client supports ${[...SUPPORTED_ATTESTATION_VERSIONS].join(', ')})`,
71
+ );
72
+ }
73
+ if (opts.contentSha256 != null && opts.artefactSha256 != null) {
74
+ throw new Error('attestation: pass either contentSha256 (v1) or artefactSha256 (v2), not both');
75
+ }
76
+
77
+ const route = v ?? (opts.artefactSha256 != null ? ATTESTATION_V2 : ATTESTATION_V1);
78
+ return route === ATTESTATION_V2 ? buildV2Attestation(opts) : buildV1Attestation(opts);
79
+ }
80
+
81
+ function buildV1Attestation({
82
+ name, version, profileHash, contentSha256, manifestSha256,
83
+ publishedAt, publisherKeyId, uploadedBy = null,
84
+ }) {
85
+ validateCommon({ name, version, profileHash, manifestSha256, publishedAt, publisherKeyId, uploadedBy });
86
+ if (typeof contentSha256 !== 'string' || !SHA256_HEX_RE.test(contentSha256)) {
87
+ throw new Error('attestation: contentSha256 must be 64 lowercase hex chars');
88
+ }
89
+ return {
90
+ wyvrnAttestationVersion: ATTESTATION_V1,
91
+ name,
92
+ version,
93
+ profileHash,
94
+ contentSha256,
95
+ manifestSha256,
96
+ publishedAt,
97
+ publisherKeyId,
98
+ uploadedBy,
99
+ };
100
+ }
101
+
102
+ function buildV2Attestation({
103
+ name, version, profileHash, artefactSha256, manifestSha256,
104
+ publishedAt, publisherKeyId, uploadedBy = null,
105
+ }) {
106
+ validateCommon({ name, version, profileHash, manifestSha256, publishedAt, publisherKeyId, uploadedBy });
107
+ if (artefactSha256 === null || typeof artefactSha256 !== 'object' || Array.isArray(artefactSha256)) {
108
+ throw new Error('attestation: artefactSha256 must be an object keyed by config name');
109
+ }
110
+ const entries = Object.entries(artefactSha256);
111
+ if (entries.length === 0) {
112
+ throw new Error(
113
+ 'attestation: artefactSha256 must not be empty (use v1 contentSha256 for fat-zip publish)',
114
+ );
115
+ }
116
+ for (const [config, sha] of entries) {
117
+ if (typeof config !== 'string' || !CONFIG_NAME_RE.test(config)) {
118
+ throw new Error(
119
+ `attestation: artefactSha256 key ${JSON.stringify(config)} is not a valid config name`,
120
+ );
121
+ }
122
+ if (typeof sha !== 'string' || !SHA256_HEX_RE.test(sha)) {
123
+ throw new Error(
124
+ `attestation: artefactSha256[${JSON.stringify(config)}] must be 64 lowercase hex chars`,
125
+ );
126
+ }
127
+ }
128
+ // Sort the in-memory shape so deepEqual et al. work intuitively in tests.
129
+ // canonicalize() sorts at signing time anyway, so this doesn't affect signed bytes.
130
+ const sortedShape = {};
131
+ for (const k of Object.keys(artefactSha256).sort()) sortedShape[k] = artefactSha256[k];
132
+
133
+ return {
134
+ wyvrnAttestationVersion: ATTESTATION_V2,
135
+ name,
136
+ version,
137
+ profileHash,
138
+ artefactSha256: sortedShape,
139
+ manifestSha256,
140
+ publishedAt,
141
+ publisherKeyId,
142
+ uploadedBy,
143
+ };
144
+ }
145
+
146
+ function validateCommon({
147
+ name, version, profileHash, manifestSha256, publishedAt, publisherKeyId, uploadedBy,
148
+ }) {
149
+ if (typeof name !== 'string' || name.length === 0) {
150
+ throw new Error('attestation: name must be a non-empty string');
151
+ }
152
+ if (typeof version !== 'string' || version.length === 0) {
153
+ throw new Error('attestation: version must be a non-empty string');
154
+ }
155
+ if (typeof profileHash !== 'string' || !PROFILE_HASH_RE.test(profileHash)) {
156
+ throw new Error(`attestation: profileHash must be 16 lowercase hex chars (got "${profileHash}")`);
157
+ }
158
+ if (typeof manifestSha256 !== 'string' || !SHA256_HEX_RE.test(manifestSha256)) {
159
+ throw new Error('attestation: manifestSha256 must be 64 lowercase hex chars');
160
+ }
161
+ if (typeof publishedAt !== 'string' || !ISO8601_RE.test(publishedAt)) {
162
+ throw new Error(`attestation: publishedAt must be ISO 8601 UTC (got "${publishedAt}")`);
163
+ }
164
+ if (typeof publisherKeyId !== 'string' || !KEY_ID_RE.test(publisherKeyId)) {
165
+ throw new Error(`attestation: publisherKeyId must match ${KEY_ID_RE} (got "${publisherKeyId}")`);
166
+ }
167
+ if (uploadedBy !== null && (typeof uploadedBy !== 'object' || Array.isArray(uploadedBy))) {
168
+ throw new Error('attestation: uploadedBy must be null or a plain object');
169
+ }
170
+ }
171
+
172
+ /**
173
+ * Parse and validate an attestation read from disk / network. Dispatches on
174
+ * `wyvrnAttestationVersion`; rejects unknown versions fail-closed (a future
175
+ * schema change must come with a verifier upgrade).
176
+ */
177
+ function parseAttestation(raw) {
178
+ let obj;
179
+ try {
180
+ obj = typeof raw === 'string' ? JSON.parse(raw) : raw;
181
+ } catch (err) {
182
+ throw new Error(`attestation: malformed JSON — ${err.message}`);
183
+ }
184
+ if (obj === null || typeof obj !== 'object' || Array.isArray(obj)) {
185
+ throw new Error('attestation: must be a JSON object');
186
+ }
187
+ const v = obj.wyvrnAttestationVersion;
188
+ if (!SUPPORTED_ATTESTATION_VERSIONS.has(v)) {
189
+ throw new Error(
190
+ `attestation: unsupported wyvrnAttestationVersion ${v}` +
191
+ ` (this client supports ${[...SUPPORTED_ATTESTATION_VERSIONS].join(', ')})`,
192
+ );
193
+ }
194
+ const required = (v === ATTESTATION_V2) ? REQUIRED_V2 : REQUIRED_V1;
195
+ for (const field of required) {
196
+ if (!(field in obj)) {
197
+ throw new Error(`attestation: missing required field "${field}"`);
198
+ }
199
+ }
200
+ return buildAttestation(obj);
201
+ }
202
+
203
+ /**
204
+ * Canonical bytes of an attestation — exactly what gets fed to ed25519.sign
205
+ * at publish and to ed25519.verify at install. Wrapping the canonicaliser
206
+ * here documents that "the signed bytes are the canonical attestation, full
207
+ * stop" without exposing canonical.js as a public API across the signing
208
+ * subsystem.
209
+ */
210
+ function canonicalAttestationBytes(att) {
211
+ return Buffer.from(canonicalize(att), 'utf8');
212
+ }
213
+
214
+ /**
215
+ * Confirm the attestation's bound `(name, version, profileHash)` match what
216
+ * the consumer expected to download. Defends against an attacker swapping a
217
+ * legitimately-signed attestation for `pkg-A@1.0.0` into the URL path of
218
+ * `pkg-B@2.0.0`. Throws on mismatch.
219
+ *
220
+ * Identical contract for v1 and v2 — both bind these fields the same way.
221
+ */
222
+ function assertBoundIdentity(att, { name, version, profileHash }) {
223
+ if (att.name !== name) {
224
+ throw new Error(`attestation bound to name="${att.name}", expected "${name}"`);
225
+ }
226
+ if (att.version !== version) {
227
+ throw new Error(`attestation bound to version="${att.version}", expected "${version}"`);
228
+ }
229
+ if (att.profileHash !== profileHash) {
230
+ throw new Error(
231
+ `attestation bound to profileHash="${att.profileHash}", expected "${profileHash}"`,
232
+ );
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Confirm bound hashes between the attestation and the consumer's local
238
+ * computation. Dispatches on `att.wyvrnAttestationVersion`:
239
+ *
240
+ * v1: expected = { contentSha256, manifestSha256 }
241
+ * v2: expected = { sliceConfig, sliceSha256, manifestSha256 }
242
+ * — sliceConfig is the canonical config name of the slice the consumer
243
+ * chose (e.g. "Release"); sliceSha256 is its locally-computed hash.
244
+ * The function looks up att.artefactSha256[sliceConfig] and asserts.
245
+ * A consumer who downloaded multiple slices calls this once per slice.
246
+ *
247
+ * manifestSha256 is checked against att.manifestSha256 in both cases so the
248
+ * single canonical-byte signature still binds the manifest the consumer read.
249
+ */
250
+ function assertBoundHashes(att, expected) {
251
+ if (att.manifestSha256 !== expected.manifestSha256) {
252
+ throw new Error(
253
+ `attestation manifestSha256="${att.manifestSha256}" does not match local manifest "${expected.manifestSha256}"`,
254
+ );
255
+ }
256
+ if (att.wyvrnAttestationVersion === ATTESTATION_V1) {
257
+ if (att.contentSha256 !== expected.contentSha256) {
258
+ throw new Error(
259
+ `attestation contentSha256="${att.contentSha256}" does not match local zip "${expected.contentSha256}"`,
260
+ );
261
+ }
262
+ return;
263
+ }
264
+ if (att.wyvrnAttestationVersion === ATTESTATION_V2) {
265
+ const { sliceConfig, sliceSha256 } = expected;
266
+ if (typeof sliceConfig !== 'string' || sliceConfig.length === 0) {
267
+ throw new Error('assertBoundHashes(v2): expected.sliceConfig required');
268
+ }
269
+ if (typeof sliceSha256 !== 'string' || !SHA256_HEX_RE.test(sliceSha256)) {
270
+ throw new Error('assertBoundHashes(v2): expected.sliceSha256 must be 64 lowercase hex chars');
271
+ }
272
+ const recorded = att.artefactSha256[sliceConfig];
273
+ if (typeof recorded !== 'string') {
274
+ throw new Error(
275
+ `attestation has no artefactSha256 entry for config "${sliceConfig}" ` +
276
+ `(published configs: ${Object.keys(att.artefactSha256).join(', ')})`,
277
+ );
278
+ }
279
+ if (recorded !== sliceSha256) {
280
+ throw new Error(
281
+ `attestation artefactSha256[${JSON.stringify(sliceConfig)}]="${recorded}" does not match local slice "${sliceSha256}"`,
282
+ );
283
+ }
284
+ return;
285
+ }
286
+ throw new Error(`assertBoundHashes: unsupported attestation version ${att.wyvrnAttestationVersion}`);
287
+ }
288
+
289
+ module.exports = {
290
+ ATTESTATION_VERSION, // legacy alias for ATTESTATION_V1
291
+ ATTESTATION_V1,
292
+ ATTESTATION_V2,
293
+ SUPPORTED_ATTESTATION_VERSIONS,
294
+ buildAttestation,
295
+ buildV1Attestation,
296
+ buildV2Attestation,
297
+ parseAttestation,
298
+ canonicalAttestationBytes,
299
+ assertBoundIdentity,
300
+ assertBoundHashes,
301
+ };
@@ -0,0 +1,80 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * Deterministic JSON serialisation: keys sorted at every nesting level, no
7
+ * whitespace, identical output on every platform and Node version.
8
+ *
9
+ * Used as the signed payload of the artefact attestation
10
+ * (claude/PLAN-SIGNING.md §3.1) and as the input to `manifestSha256` so the
11
+ * published manifest cannot be swapped while the zip stays the same.
12
+ *
13
+ * Behavioural notes:
14
+ * - Object keys sorted via `Array.prototype.sort()` (UTF-16 code-unit order).
15
+ * Our attestation + manifest keys are all ASCII, so this matches the
16
+ * RFC 8785 (JCS) ordering that future cross-tool verifiers expect.
17
+ * - Primitive serialisation is delegated to `JSON.stringify`, which uses
18
+ * ES262 `ToString(Number)` for numbers — the same algorithm JCS mandates.
19
+ * - Non-finite numbers (NaN, Infinity, -Infinity) are rejected.
20
+ * - `undefined`, functions, and symbols are rejected. JSON has no
21
+ * representation for them; silently dropping (the default
22
+ * `JSON.stringify` behaviour) would change the signed bytes when a
23
+ * caller forgot to populate a field.
24
+ * - Arrays preserve their input order — only object keys get sorted.
25
+ */
26
+ function canonicalize(value) {
27
+ return _serialize(value, []);
28
+ }
29
+
30
+ function _serialize(value, pathStack) {
31
+ if (value === null) return 'null';
32
+
33
+ const t = typeof value;
34
+ if (t === 'boolean') return value ? 'true' : 'false';
35
+
36
+ if (t === 'number') {
37
+ if (!Number.isFinite(value)) {
38
+ throw new Error(
39
+ `canonicalize: non-finite number at ${_pathStr(pathStack)} (${value})`,
40
+ );
41
+ }
42
+ return JSON.stringify(value);
43
+ }
44
+
45
+ if (t === 'string') return JSON.stringify(value);
46
+
47
+ if (Array.isArray(value)) {
48
+ const parts = value.map((v, i) => _serialize(v, [...pathStack, i]));
49
+ return '[' + parts.join(',') + ']';
50
+ }
51
+
52
+ if (t === 'object') {
53
+ const keys = Object.keys(value).sort();
54
+ const parts = keys.map((k) =>
55
+ JSON.stringify(k) + ':' + _serialize(value[k], [...pathStack, k]),
56
+ );
57
+ return '{' + parts.join(',') + '}';
58
+ }
59
+
60
+ throw new Error(
61
+ `canonicalize: unsupported value at ${_pathStr(pathStack)} (typeof=${t})`,
62
+ );
63
+ }
64
+
65
+ function _pathStr(stack) {
66
+ return stack.length === 0 ? '<root>' : stack.map((s) => `[${JSON.stringify(s)}]`).join('');
67
+ }
68
+
69
+ /**
70
+ * Convenience: SHA256 of the canonical form, full 64-char lowercase hex.
71
+ * The shape consumers cite as `manifestSha256` in the attestation block.
72
+ */
73
+ function sha256OfCanonical(value) {
74
+ return crypto.createHash('sha256').update(canonicalize(value), 'utf8').digest('hex');
75
+ }
76
+
77
+ module.exports = {
78
+ canonicalize,
79
+ sha256OfCanonical,
80
+ };
@@ -0,0 +1,123 @@
1
+ 'use strict';
2
+
3
+ const crypto = require('crypto');
4
+
5
+ /**
6
+ * Thin wrappers over Node's built-in `crypto` for Ed25519 signing and key
7
+ * handling. The whole reason wyvrnpm picked Ed25519 (vs GPG, Sigstore,
8
+ * Authenticode) is that nothing here shells out — every byte goes through
9
+ * `node:crypto`, identical on Windows / Linux / macOS.
10
+ *
11
+ * Format conventions (mirror what `keys.json` and the publisher's local
12
+ * `*.pub.pem` / `*.priv.pem` use):
13
+ *
14
+ * - Private keys round-trip as PKCS#8 PEM (Node's native format for
15
+ * `createPrivateKey`).
16
+ * - Public keys round-trip as SPKI PEM for file storage and as SPKI DER
17
+ * base64 for embedding in `keys.json`. Both forms are accepted by the
18
+ * verify path; pick whichever is more ergonomic at the call site.
19
+ * - Signatures are the raw 64-byte Ed25519 output (RFC 8032). No DER
20
+ * wrapping. `wyvrn.sig` carries the base64 form.
21
+ */
22
+
23
+ const ALG = 'ed25519';
24
+ const RAW_SIG_LEN = 64;
25
+
26
+ function generateKeyPair() {
27
+ const { publicKey, privateKey } = crypto.generateKeyPairSync(ALG);
28
+ return {
29
+ privateKeyPem: privateKey.export({ format: 'pem', type: 'pkcs8' }),
30
+ publicKeyPem: publicKey.export({ format: 'pem', type: 'spki' }),
31
+ publicKeyB64: publicKey.export({ format: 'der', type: 'spki' }).toString('base64'),
32
+ };
33
+ }
34
+
35
+ /**
36
+ * Load a private key from a PEM string. Validates that the key is Ed25519;
37
+ * other algorithms throw fast so a misconfigured publisher can't accidentally
38
+ * publish with a wrong-shape signature.
39
+ */
40
+ function loadPrivateKey(pem) {
41
+ const k = crypto.createPrivateKey({ key: pem, format: 'pem' });
42
+ if (k.asymmetricKeyType !== ALG) {
43
+ throw new Error(`expected ed25519 private key, got ${k.asymmetricKeyType}`);
44
+ }
45
+ return k;
46
+ }
47
+
48
+ /**
49
+ * Load a public key from either an SPKI PEM string (file-based) or a base64
50
+ * SPKI DER string (registry `keys.json`). Auto-detects by leading marker.
51
+ */
52
+ function loadPublicKey(input) {
53
+ if (typeof input !== 'string' || input.length === 0) {
54
+ throw new Error('loadPublicKey: input must be a non-empty string');
55
+ }
56
+ let k;
57
+ if (input.includes('-----BEGIN')) {
58
+ k = crypto.createPublicKey({ key: input, format: 'pem' });
59
+ } else {
60
+ const der = Buffer.from(input, 'base64');
61
+ k = crypto.createPublicKey({ key: der, format: 'der', type: 'spki' });
62
+ }
63
+ if (k.asymmetricKeyType !== ALG) {
64
+ throw new Error(`expected ed25519 public key, got ${k.asymmetricKeyType}`);
65
+ }
66
+ return k;
67
+ }
68
+
69
+ /**
70
+ * Derive the public key (SPKI PEM + base64 DER) from a private key PEM.
71
+ * Used by `wyvrnpm key show-pub` and as a sanity check during publisher
72
+ * bootstrap.
73
+ */
74
+ function derivePublicKey(privateKeyPem) {
75
+ const priv = loadPrivateKey(privateKeyPem);
76
+ const pub = crypto.createPublicKey(priv);
77
+ return {
78
+ publicKeyPem: pub.export({ format: 'pem', type: 'spki' }),
79
+ publicKeyB64: pub.export({ format: 'der', type: 'spki' }).toString('base64'),
80
+ };
81
+ }
82
+
83
+ /**
84
+ * Sign `bytes` with `privateKey`. `privateKey` may be a PEM string or an
85
+ * already-loaded KeyObject. Returns a 64-byte raw Ed25519 signature.
86
+ */
87
+ function sign(privateKey, bytes) {
88
+ const key = typeof privateKey === 'string' ? loadPrivateKey(privateKey) : privateKey;
89
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes, 'utf8');
90
+ // Ed25519 in Node uses null algorithm (key type carries the algorithm).
91
+ const sig = crypto.sign(null, buf, key);
92
+ if (sig.length !== RAW_SIG_LEN) {
93
+ throw new Error(`unexpected ed25519 signature length: ${sig.length}`);
94
+ }
95
+ return sig;
96
+ }
97
+
98
+ /**
99
+ * Verify `sig` against `bytes` using `publicKey`. `publicKey` may be a PEM
100
+ * string, a base64 SPKI DER string, or a KeyObject. `sig` may be a Buffer or
101
+ * a base64 string. Returns boolean — never throws on a bad signature, only
102
+ * on a malformed key/sig shape.
103
+ */
104
+ function verify(publicKey, bytes, sig) {
105
+ const key = typeof publicKey === 'string' ? loadPublicKey(publicKey) : publicKey;
106
+ const buf = Buffer.isBuffer(bytes) ? bytes : Buffer.from(bytes, 'utf8');
107
+ const sigBuf = Buffer.isBuffer(sig) ? sig : Buffer.from(sig, 'base64');
108
+ if (sigBuf.length !== RAW_SIG_LEN) {
109
+ throw new Error(`expected ${RAW_SIG_LEN}-byte ed25519 signature, got ${sigBuf.length}`);
110
+ }
111
+ return crypto.verify(null, buf, key, sigBuf);
112
+ }
113
+
114
+ module.exports = {
115
+ ALG,
116
+ RAW_SIG_LEN,
117
+ generateKeyPair,
118
+ loadPrivateKey,
119
+ loadPublicKey,
120
+ derivePublicKey,
121
+ sign,
122
+ verify,
123
+ };