@jinn-network/core 0.1.0 → 0.1.1-canary.7b188db5
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/dist/corpus-read/ipfs.d.ts +10 -1
- package/dist/corpus-read/ipfs.js +14 -5
- package/dist/scrub/apply-dispositions.d.ts +74 -0
- package/dist/scrub/apply-dispositions.js +309 -0
- package/dist/scrub/build.d.ts +54 -31
- package/dist/scrub/build.js +114 -55
- package/dist/scrub/checksummed-instruments-detector.d.ts +13 -0
- package/dist/scrub/checksummed-instruments-detector.js +119 -0
- package/dist/scrub/data/bip39-english.d.ts +2 -0
- package/dist/scrub/data/bip39-english.js +2051 -0
- package/dist/scrub/data/gitleaks-rules.d.ts +56 -0
- package/dist/scrub/data/gitleaks-rules.js +67 -0
- package/dist/scrub/emit-scrub.d.ts +13 -2
- package/dist/scrub/emit-scrub.js +39 -4
- package/dist/scrub/eval/findings-from-scrub.d.ts +18 -0
- package/dist/scrub/eval/findings-from-scrub.js +99 -0
- package/dist/scrub/eval/fixtures.d.ts +14 -0
- package/dist/scrub/eval/fixtures.js +159 -0
- package/dist/scrub/eval/index.d.ts +6 -0
- package/dist/scrub/eval/index.js +6 -0
- package/dist/scrub/eval/local-corpus.d.ts +15 -0
- package/dist/scrub/eval/local-corpus.js +29 -0
- package/dist/scrub/eval/metrics.d.ts +19 -0
- package/dist/scrub/eval/metrics.js +77 -0
- package/dist/scrub/eval/run-bench.d.ts +8 -0
- package/dist/scrub/eval/run-bench.js +125 -0
- package/dist/scrub/eval/types.d.ts +69 -0
- package/dist/scrub/eval/types.js +7 -0
- package/dist/scrub/finding.d.ts +38 -0
- package/dist/scrub/finding.js +7 -0
- package/dist/scrub/git-identity-detector.d.ts +16 -0
- package/dist/scrub/git-identity-detector.js +116 -0
- package/dist/scrub/gitleaks-detector.d.ts +13 -0
- package/dist/scrub/gitleaks-detector.js +45 -0
- package/dist/scrub/gliner-detector.d.ts +65 -0
- package/dist/scrub/gliner-detector.js +129 -0
- package/dist/scrub/index.d.ts +15 -1
- package/dist/scrub/index.js +15 -1
- package/dist/scrub/ip-address-detector.d.ts +12 -0
- package/dist/scrub/ip-address-detector.js +81 -0
- package/dist/scrub/key-policy.d.ts +18 -8
- package/dist/scrub/key-policy.js +49 -19
- package/dist/scrub/known-identity-detector.d.ts +66 -0
- package/dist/scrub/known-identity-detector.js +284 -0
- package/dist/scrub/layer2.d.ts +13 -18
- package/dist/scrub/layer2.js +22 -26
- package/dist/scrub/ml-pii-stage.d.ts +28 -10
- package/dist/scrub/ml-pii-stage.js +100 -23
- package/dist/scrub/pii-build.d.ts +17 -8
- package/dist/scrub/pii-build.js +9 -9
- package/dist/scrub/pipeline.d.ts +63 -24
- package/dist/scrub/pipeline.js +180 -38
- package/dist/scrub/plain-patterns-stage.d.ts +21 -19
- package/dist/scrub/plain-patterns-stage.js +61 -57
- package/dist/scrub/policy.d.ts +28 -0
- package/dist/scrub/policy.js +156 -0
- package/dist/scrub/provenance.d.ts +53 -0
- package/dist/scrub/provenance.js +83 -0
- package/dist/scrub/reject-classes-detector.d.ts +11 -0
- package/dist/scrub/reject-classes-detector.js +195 -0
- package/dist/scrub/reject-publish-error.d.ts +30 -0
- package/dist/scrub/reject-publish-error.js +47 -0
- package/dist/scrub/review-queue.d.ts +90 -0
- package/dist/scrub/review-queue.js +259 -0
- package/dist/scrub/secretlint-stage.d.ts +11 -26
- package/dist/scrub/secretlint-stage.js +45 -30
- package/dist/scrub/transformers-detector.d.ts +15 -9
- package/dist/scrub/transformers-detector.js +20 -10
- package/dist/scrub/types.d.ts +11 -0
- package/dist/scrub/url-credentials-detector.d.ts +9 -0
- package/dist/scrub/url-credentials-detector.js +66 -0
- package/dist/trajectory/schema.d.ts +75 -0
- package/dist/trajectory/schema.js +9 -0
- package/package.json +8 -3
- package/dist/scrub/openredaction-stage.d.ts +0 -196
- package/dist/scrub/openredaction-stage.js +0 -270
package/dist/scrub/build.js
CHANGED
|
@@ -1,9 +1,17 @@
|
|
|
1
1
|
import { ScrubPipeline } from './pipeline.js';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
import {
|
|
2
|
+
import { keyPolicyDetector } from './key-policy.js';
|
|
3
|
+
import { plainPatternsDetector } from './plain-patterns-stage.js';
|
|
4
|
+
import { gitIdentityDetector } from './git-identity-detector.js';
|
|
5
|
+
import { secretlintDetector } from './secretlint-stage.js';
|
|
6
|
+
import { mlPiiDetector } from './ml-pii-stage.js';
|
|
7
|
+
import { assembleKnownIdentity, knownIdentityDetector, } from './known-identity-detector.js';
|
|
8
|
+
import { urlCredentialsDetector } from './url-credentials-detector.js';
|
|
9
|
+
import { rejectClassesDetector } from './reject-classes-detector.js';
|
|
10
|
+
import { checksummedInstrumentsDetector } from './checksummed-instruments-detector.js';
|
|
11
|
+
import { ipAddressDetector } from './ip-address-detector.js';
|
|
12
|
+
import { gitleaksDetector } from './gitleaks-detector.js';
|
|
13
|
+
import { DEFAULT_POLICY } from './policy.js';
|
|
14
|
+
import { computeAllowlistDigest } from './provenance.js';
|
|
7
15
|
/**
|
|
8
16
|
* Default key policy. `jinn.*` identity/chain attributes are structural and pass
|
|
9
17
|
* raw. The `drop` tier deletes the spec's stage-1 high-confidence keys outright
|
|
@@ -11,8 +19,9 @@ import { mlPiiStage } from './ml-pii-stage.js';
|
|
|
11
19
|
* these never carry sellable content and are never safe to publish. Globs use the
|
|
12
20
|
* trailing-`*` prefix form `classifyKey` supports; the HTTP header keys are listed
|
|
13
21
|
* per request/response direction (a leading `*.header.…` glob is not matched, so
|
|
14
|
-
* we enumerate the concrete keys).
|
|
15
|
-
*
|
|
22
|
+
* we enumerate the concrete keys). Machine-identity keys (D3 carrier) drop
|
|
23
|
+
* attempt-manifest `host` / hostname telemetry. Everything else is `content` and
|
|
24
|
+
* flows through the value-scrubbing detectors.
|
|
16
25
|
*/
|
|
17
26
|
export const DEFAULT_KEY_POLICY = {
|
|
18
27
|
safe: ['jinn.*'],
|
|
@@ -25,60 +34,110 @@ export const DEFAULT_KEY_POLICY = {
|
|
|
25
34
|
'http.response.header.set-cookie',
|
|
26
35
|
'env.*',
|
|
27
36
|
],
|
|
37
|
+
machineIdentity: [
|
|
38
|
+
'host',
|
|
39
|
+
'hostname',
|
|
40
|
+
'os.hostname',
|
|
41
|
+
'attempt.host',
|
|
42
|
+
'attempt.hostname',
|
|
43
|
+
'system.hostname',
|
|
44
|
+
'device.hostname',
|
|
45
|
+
'device.host',
|
|
46
|
+
],
|
|
28
47
|
};
|
|
48
|
+
export function resolveKnownIdentity(opts) {
|
|
49
|
+
// AssembledKnownIdentity is a subset of AssembleKnownIdentityOptions;
|
|
50
|
+
// re-assembly is idempotent (allowlist entries are deduped).
|
|
51
|
+
return assembleKnownIdentity(opts);
|
|
52
|
+
}
|
|
53
|
+
/** Duck-type ML detector metadata for the policy hash (#1974). */
|
|
54
|
+
function provenanceFromPiiDetector(detector) {
|
|
55
|
+
if (!detector)
|
|
56
|
+
return { modelId: null, labels: [] };
|
|
57
|
+
const withMeta = detector;
|
|
58
|
+
if (typeof withMeta.modelId === 'string' && withMeta.labelSet) {
|
|
59
|
+
return { modelId: withMeta.modelId, labels: withMeta.labelSet };
|
|
60
|
+
}
|
|
61
|
+
return { modelId: null, labels: [] };
|
|
62
|
+
}
|
|
63
|
+
/** Build provenance extras from known-identity + optional ML detector (#1974). */
|
|
64
|
+
export function buildProvenanceExtras(knownIdentity, piiDetector) {
|
|
65
|
+
const ml = provenanceFromPiiDetector(piiDetector);
|
|
66
|
+
return {
|
|
67
|
+
modelId: ml.modelId,
|
|
68
|
+
labels: ml.labels,
|
|
69
|
+
allowlistDigest: computeAllowlistDigest(knownIdentity.allowlist.entries),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Shared detector inventory (#1969 / #1970 / #1971 / #1972 / #1973). Every
|
|
74
|
+
* publish/check consumer runs the same owned detectors (key-policy,
|
|
75
|
+
* plain-patterns including C1 wallet + A1 credential IDs, git-identity B2
|
|
76
|
+
* carriers, known-identity pack + non-address allowlist, Tier-1
|
|
77
|
+
* reject/URL/IP/instruments/gitleaks, secretlint). What varies is disposition /
|
|
78
|
+
* check-mode and whether an ML PII detector is injected. Seed keeps the entropy
|
|
79
|
+
* fallback off until A2 mid-band flags are absorbed without refuse-on-detection
|
|
80
|
+
* (#1409 shipped behavior). openredaction was retired in #1973.
|
|
81
|
+
*/
|
|
82
|
+
export function sharedDetectorInventory(policy, opts = {}) {
|
|
83
|
+
const detectors = [keyPolicyDetector(policy)];
|
|
84
|
+
detectors.push(plainPatternsDetector(policy));
|
|
85
|
+
detectors.push(gitIdentityDetector(policy));
|
|
86
|
+
detectors.push(knownIdentityDetector(policy, { assembled: resolveKnownIdentity(opts.knownIdentity) }));
|
|
87
|
+
detectors.push(urlCredentialsDetector(policy));
|
|
88
|
+
detectors.push(rejectClassesDetector(policy));
|
|
89
|
+
detectors.push(checksummedInstrumentsDetector(policy));
|
|
90
|
+
detectors.push(ipAddressDetector(policy));
|
|
91
|
+
detectors.push(gitleaksDetector(policy));
|
|
92
|
+
detectors.push(secretlintDetector(policy, { entropyFallback: opts.entropyFallback ?? true }));
|
|
93
|
+
if (opts.piiDetector) {
|
|
94
|
+
detectors.push(mlPiiDetector(policy, opts.piiDetector));
|
|
95
|
+
}
|
|
96
|
+
return detectors;
|
|
97
|
+
}
|
|
29
98
|
/**
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
* the ML model is unavailable.
|
|
99
|
+
* Trace / redact-mode preset over the shared inventory.
|
|
100
|
+
*
|
|
101
|
+
* @deprecated Prefer thinking in terms of one inventory + policy; this builder
|
|
102
|
+
* remains as a compatibility preset through the migration (#1969).
|
|
35
103
|
*/
|
|
36
104
|
export function buildScrubPipeline(opts = {}) {
|
|
37
105
|
const policy = opts.policy ?? DEFAULT_KEY_POLICY;
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
stages.push(mlPiiStage(policy, piiDetector));
|
|
51
|
-
}
|
|
52
|
-
return stages;
|
|
106
|
+
const knownIdentity = resolveKnownIdentity(opts.knownIdentity);
|
|
107
|
+
return new ScrubPipeline(sharedDetectorInventory(policy, {
|
|
108
|
+
entropyFallback: true,
|
|
109
|
+
piiDetector: opts.piiDetector,
|
|
110
|
+
knownIdentity,
|
|
111
|
+
}), {
|
|
112
|
+
policy: DEFAULT_POLICY,
|
|
113
|
+
checkMode: false,
|
|
114
|
+
reviewStore: opts.reviewStore,
|
|
115
|
+
failClosedOnUnresolvedFlags: opts.failClosedOnUnresolvedFlags,
|
|
116
|
+
provenance: buildProvenanceExtras(knownIdentity, opts.piiDetector),
|
|
117
|
+
});
|
|
53
118
|
}
|
|
54
119
|
/**
|
|
55
|
-
* Seed-
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
* long camelCase identifiers) and deface the corpus. The deterministic
|
|
61
|
-
* detectors stay:
|
|
62
|
-
* structural key policy, plain-patterns (emails, home paths, and — seed-only,
|
|
63
|
-
* #1415 — bare AWS access-key IDs and GCP `AIza…` API keys, deterministic
|
|
64
|
-
* prefix shapes secretlint pass-1 does not cover), and secretlint's pass-1
|
|
65
|
-
* preset rules (AWS secret-key assignments, GitHub / Slack / npm token
|
|
66
|
-
* shapes, GCP service-account JSON). Accepted residual: JWTs and unprefixed
|
|
67
|
-
* high-entropy blobs (trace profile catches them via the entropy fallback),
|
|
68
|
-
* plus every structured identifier or PII class detected only by the omitted
|
|
69
|
-
* openredaction stage, pass unredacted. The latter is a 570+ pattern surface;
|
|
70
|
-
* payment cards, phone numbers, SSNs, medical or health-plan identifiers,
|
|
71
|
-
* government identity documents, and financial account references are
|
|
72
|
-
* examples, not an exhaustive allowlist. This is acceptable only for public
|
|
73
|
-
* seed material that a curator has transformed and reviewed for those classes.
|
|
74
|
-
* The reduced stage list is inspectable locally through the pipeline's
|
|
75
|
-
* `components` surface. `TraceEnvelopeV0` does not publish that list, so a
|
|
76
|
-
* fetched envelope cannot by itself prove which scrub profile ran.
|
|
120
|
+
* Seed / redact-mode preset (#1409 / #1969). Same owned inventory as layer-2.
|
|
121
|
+
* Entropy fallback stays off to preserve the shipped zero-corruption seed
|
|
122
|
+
* behavior until A2 mid-band can flag without tripping refuse-on-detection.
|
|
123
|
+
*
|
|
124
|
+
* @deprecated Compatibility preset over the one inventory + policy table.
|
|
77
125
|
*/
|
|
78
|
-
export function buildSeedScrubPipeline(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
126
|
+
export function buildSeedScrubPipeline(policyOrOpts = DEFAULT_KEY_POLICY) {
|
|
127
|
+
const opts = policyOrOpts && 'safe' in policyOrOpts
|
|
128
|
+
? { policy: policyOrOpts }
|
|
129
|
+
: policyOrOpts;
|
|
130
|
+
const policy = opts.policy ?? DEFAULT_KEY_POLICY;
|
|
131
|
+
const knownIdentity = resolveKnownIdentity(opts.knownIdentity);
|
|
132
|
+
return new ScrubPipeline(sharedDetectorInventory(policy, {
|
|
133
|
+
entropyFallback: false,
|
|
134
|
+
piiDetector: opts.piiDetector,
|
|
135
|
+
knownIdentity,
|
|
136
|
+
}), {
|
|
137
|
+
policy: DEFAULT_POLICY,
|
|
138
|
+
checkMode: false,
|
|
139
|
+
reviewStore: opts.reviewStore,
|
|
140
|
+
failClosedOnUnresolvedFlags: opts.failClosedOnUnresolvedFlags,
|
|
141
|
+
provenance: buildProvenanceExtras(knownIdentity, opts.piiDetector),
|
|
142
|
+
});
|
|
84
143
|
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B7 — checksummed payment instruments (#1972 / design §6.2).
|
|
3
|
+
*
|
|
4
|
+
* Card numbers (Luhn) and IBANs (mod-97). SSN-shape stays context-gated and is
|
|
5
|
+
* not emitted here.
|
|
6
|
+
*/
|
|
7
|
+
import { type KeyPolicy } from './key-policy.js';
|
|
8
|
+
import type { Detector } from './finding.js';
|
|
9
|
+
export declare function checksummedInstrumentsDetector(policy: KeyPolicy): Detector;
|
|
10
|
+
/** ISO/IEC 7812 Luhn checksum. */
|
|
11
|
+
export declare function luhnOk(digits: string): boolean;
|
|
12
|
+
/** ISO 13616 IBAN mod-97 check. */
|
|
13
|
+
export declare function ibanMod97Ok(iban: string): boolean;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* B7 — checksummed payment instruments (#1972 / design §6.2).
|
|
3
|
+
*
|
|
4
|
+
* Card numbers (Luhn) and IBANs (mod-97). SSN-shape stays context-gated and is
|
|
5
|
+
* not emitted here.
|
|
6
|
+
*/
|
|
7
|
+
import { classifyKey } from './key-policy.js';
|
|
8
|
+
const VERSION = '1.0.0';
|
|
9
|
+
/** 13–19 digit runs, optional spaces/dashes between digit groups. */
|
|
10
|
+
const CARD_CANDIDATE = /\b(?:\d[ -]*?){13,19}\b/g;
|
|
11
|
+
/** IBAN: country + check digits + BBAN (no internal spaces in the candidate). */
|
|
12
|
+
const IBAN_CANDIDATE = /\b[A-Z]{2}\d{2}[A-Z0-9]{11,30}\b/gi;
|
|
13
|
+
/** Spaced IBAN groups (e.g. GB82 WEST 1234 5698 7654 32). */
|
|
14
|
+
const IBAN_SPACED = /\b[A-Z]{2}\d{2}(?:[ ][A-Z0-9]{4}){2,7}(?:[ ][A-Z0-9]{1,4})?\b/gi;
|
|
15
|
+
export function checksummedInstrumentsDetector(policy) {
|
|
16
|
+
const meta = { name: 'checksummed-instruments', version: VERSION };
|
|
17
|
+
return {
|
|
18
|
+
...meta,
|
|
19
|
+
detect(attributes) {
|
|
20
|
+
const findings = [];
|
|
21
|
+
for (const [key, value] of Object.entries(attributes)) {
|
|
22
|
+
if (typeof value !== 'string' || classifyKey(key, policy) !== 'content')
|
|
23
|
+
continue;
|
|
24
|
+
findings.push(...scanCards(value, key, meta));
|
|
25
|
+
findings.push(...scanIbans(value, key, meta));
|
|
26
|
+
}
|
|
27
|
+
return findings;
|
|
28
|
+
},
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function scanCards(text, key, detector) {
|
|
32
|
+
const findings = [];
|
|
33
|
+
const re = new RegExp(CARD_CANDIDATE.source, CARD_CANDIDATE.flags);
|
|
34
|
+
let m;
|
|
35
|
+
while ((m = re.exec(text)) !== null) {
|
|
36
|
+
const raw = m[0];
|
|
37
|
+
const digits = raw.replace(/\D/g, '');
|
|
38
|
+
if (digits.length < 13 || digits.length > 19)
|
|
39
|
+
continue;
|
|
40
|
+
// Skip obvious non-cards (all same digit, leading zeros-only short ids).
|
|
41
|
+
if (/^(\d)\1+$/.test(digits))
|
|
42
|
+
continue;
|
|
43
|
+
if (!luhnOk(digits))
|
|
44
|
+
continue;
|
|
45
|
+
findings.push({
|
|
46
|
+
class: 'B7',
|
|
47
|
+
span: { key, start: m.index, end: m.index + raw.length },
|
|
48
|
+
confidence: 'VERY_HIGH',
|
|
49
|
+
evidence: ['card-luhn'],
|
|
50
|
+
detector,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
return findings;
|
|
54
|
+
}
|
|
55
|
+
function scanIbans(text, key, detector) {
|
|
56
|
+
const findings = [];
|
|
57
|
+
const seen = new Set();
|
|
58
|
+
for (const pattern of [IBAN_CANDIDATE, IBAN_SPACED]) {
|
|
59
|
+
const re = new RegExp(pattern.source, pattern.flags);
|
|
60
|
+
let m;
|
|
61
|
+
while ((m = re.exec(text)) !== null) {
|
|
62
|
+
const raw = m[0];
|
|
63
|
+
const compact = raw.replace(/\s+/g, '').toUpperCase();
|
|
64
|
+
if (compact.length < 15 || compact.length > 34)
|
|
65
|
+
continue;
|
|
66
|
+
if (!ibanMod97Ok(compact))
|
|
67
|
+
continue;
|
|
68
|
+
const spanKey = `${m.index}:${m.index + raw.length}`;
|
|
69
|
+
if (seen.has(spanKey))
|
|
70
|
+
continue;
|
|
71
|
+
seen.add(spanKey);
|
|
72
|
+
findings.push({
|
|
73
|
+
class: 'B7',
|
|
74
|
+
span: { key, start: m.index, end: m.index + raw.length },
|
|
75
|
+
confidence: 'VERY_HIGH',
|
|
76
|
+
evidence: ['iban-mod97'],
|
|
77
|
+
detector,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return findings;
|
|
82
|
+
}
|
|
83
|
+
/** ISO/IEC 7812 Luhn checksum. */
|
|
84
|
+
export function luhnOk(digits) {
|
|
85
|
+
let sum = 0;
|
|
86
|
+
let alt = false;
|
|
87
|
+
for (let i = digits.length - 1; i >= 0; i -= 1) {
|
|
88
|
+
let n = digits.charCodeAt(i) - 48;
|
|
89
|
+
if (n < 0 || n > 9)
|
|
90
|
+
return false;
|
|
91
|
+
if (alt) {
|
|
92
|
+
n *= 2;
|
|
93
|
+
if (n > 9)
|
|
94
|
+
n -= 9;
|
|
95
|
+
}
|
|
96
|
+
sum += n;
|
|
97
|
+
alt = !alt;
|
|
98
|
+
}
|
|
99
|
+
return sum % 10 === 0;
|
|
100
|
+
}
|
|
101
|
+
/** ISO 13616 IBAN mod-97 check. */
|
|
102
|
+
export function ibanMod97Ok(iban) {
|
|
103
|
+
const rearranged = iban.slice(4) + iban.slice(0, 4);
|
|
104
|
+
let expanded = '';
|
|
105
|
+
for (const ch of rearranged) {
|
|
106
|
+
if (ch >= '0' && ch <= '9')
|
|
107
|
+
expanded += ch;
|
|
108
|
+
else if (ch >= 'A' && ch <= 'Z')
|
|
109
|
+
expanded += String(ch.charCodeAt(0) - 55);
|
|
110
|
+
else
|
|
111
|
+
return false;
|
|
112
|
+
}
|
|
113
|
+
// Progressive mod to avoid BigInt dependency for typical IBAN lengths.
|
|
114
|
+
let rest = 0;
|
|
115
|
+
for (const ch of expanded) {
|
|
116
|
+
rest = (rest * 10 + (ch.charCodeAt(0) - 48)) % 97;
|
|
117
|
+
}
|
|
118
|
+
return rest === 1;
|
|
119
|
+
}
|