@myazahq/kyc-sdk-react-native 2.2.0 → 2.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.
@@ -1,17 +1,23 @@
1
- import React from 'react';
2
- import { View } from 'react-native';
1
+ import React, { useState } from 'react';
2
+ import { PixelRatio, useWindowDimensions, View, type LayoutChangeEvent } from 'react-native';
3
3
 
4
4
  import { spacing } from '../config/theme';
5
+ import { fitStepCircles, windowedSteps, type StepSlot } from '../lib/step-window';
5
6
  import { useTheme } from './runtime';
6
7
  import { MyazaText } from './Typography';
7
8
  import { Icon } from './Icon';
8
9
 
9
10
  // Segmented numbered step indicator — 1:1 with the Flutter SDK's _StepIndicator.
10
- // N circles connected by thin lines:
11
- // • completed → filled primary + white check
12
- // • active → filled primary + white number
11
+ // N numbered circles connected by thin lines:
12
+ // • completed → filled primary + number + a check BADGE
13
+ // • active → filled primary + number
13
14
  // • upcoming → outlined (primary200) + muted number
14
15
  // activeIndex = round(progress * stepCount) - 1.
16
+ //
17
+ // Long flows COLLAPSE rather than overflow, but only as a LAST RESORT: the row
18
+ // measures itself and shows as many real circles as the width allows, windowing
19
+ // around the current step (first and last always shown) only once the connectors
20
+ // would stop reading as a chain. See lib/step-window.
15
21
 
16
22
  export interface StepIndicatorProps {
17
23
  /** 0.0–1.0 progress fraction. */
@@ -19,47 +25,155 @@ export interface StepIndicatorProps {
19
25
  stepCount: number;
20
26
  }
21
27
 
28
+ /** Base circle size, before text scaling. */
29
+ const CIRCLE = 26;
30
+
22
31
  export function StepIndicator({ progress, stepCount }: StepIndicatorProps): React.ReactElement {
23
32
  const { colors } = useTheme();
24
33
  const active = Math.round(progress * stepCount) - 1;
25
34
 
35
+ // Grow the circle with the system text size, or the number inside it clips
36
+ // the moment a user turns Dynamic Type up — the circle was a hard 26px while
37
+ // the label scaled freely. Capped at 1.4: past that the row matters less than
38
+ // the step content below it, and an indicator that pushes the title off screen
39
+ // helps nobody.
40
+ const scale = Math.min(PixelRatio.getFontScale(), 1.4);
41
+ const size = Math.round(CIRCLE * scale);
42
+ const badge = Math.round(13 * scale);
43
+
44
+ // How many circles actually fit. Measured rather than assumed, so a flow
45
+ // collapses on a narrow phone and stays whole on a wide one instead of both
46
+ // obeying the same hardcoded cap. The window width is the first estimate — it
47
+ // is right on the first frame, which avoids rendering a collapsed row and then
48
+ // snapping to a full one — and onLayout corrects it if the row is inset by
49
+ // anything this does not know about.
50
+ const { width: screenWidth } = useWindowDimensions();
51
+ const [measured, setMeasured] = useState(0);
52
+ const rowWidth = measured || Math.max(0, screenWidth - spacing.md * 2);
53
+ const slots = windowedSteps(stepCount, active, fitStepCircles(rowWidth, size));
54
+
55
+ const onLayout = (e: LayoutChangeEvent): void => {
56
+ const w = e.nativeEvent.layout.width - spacing.md * 2;
57
+ // Only react to real changes (rotation, split screen). Setting state on
58
+ // every layout pass would re-render the row for nothing.
59
+ setMeasured((prev) => (Math.abs(prev - w) > 1 ? w : prev));
60
+ };
61
+
26
62
  return (
27
- <View style={{ flexDirection: 'row', alignItems: 'center', paddingHorizontal: spacing.md }}>
28
- {Array.from({ length: stepCount }).map((_, i) => {
29
- const completed = i < active;
30
- const isActive = i === active;
63
+ <View
64
+ style={{ flexDirection: 'row', alignItems: 'center', paddingHorizontal: spacing.md }}
65
+ onLayout={onLayout}
66
+ // ONE label for the whole row. Previously a screen reader walked ten
67
+ // unlabelled circles and announced a bare "6", which says nothing about
68
+ // how far through the flow that is.
69
+ accessible
70
+ accessibilityRole="progressbar"
71
+ accessibilityLabel={`Step ${Math.min(Math.max(active + 1, 1), stepCount)} of ${stepCount}`}
72
+ accessibilityValue={{
73
+ min: 1,
74
+ max: stepCount,
75
+ now: Math.min(Math.max(active + 1, 1), stepCount),
76
+ }}
77
+ >
78
+ {slots.map((slot: StepSlot, position) => {
79
+ const isEllipsis = slot === 'ellipsis';
80
+ const completed = !isEllipsis && slot < active;
81
+ const isActive = !isEllipsis && slot === active;
31
82
  const filled = completed || isActive;
32
83
  return (
33
- <React.Fragment key={i}>
34
- <View
35
- style={{
36
- width: 26,
37
- height: 26,
38
- borderRadius: 13,
39
- alignItems: 'center',
40
- justifyContent: 'center',
41
- backgroundColor: filled ? colors.primary : 'transparent',
42
- borderWidth: 1.5,
43
- borderColor: filled ? colors.primary : colors.primary200,
44
- }}
45
- >
46
- {completed ? (
47
- <Icon name="check" size={14} color="#FFFFFF" />
48
- ) : (
84
+ <React.Fragment key={isEllipsis ? `gap-${position}` : `step-${slot}`}>
85
+ {isEllipsis ? (
86
+ // Collapsed run. Sized to the circle's height so the connectors on
87
+ // either side stay on the same centre line and the chain reads as
88
+ // continuous rather than broken in two.
89
+ <View
90
+ style={{ height: size, justifyContent: 'center', paddingHorizontal: 2 }}
91
+ importantForAccessibility="no"
92
+ >
49
93
  <MyazaText
50
94
  variant="bodySmall"
51
- color={isActive ? '#FFFFFF' : colors.textMuted}
52
- style={{ fontSize: 11, fontWeight: '700' }}
95
+ color={colors.textMuted}
96
+ style={{ fontSize: 13, letterSpacing: 1 }}
97
+ allowFontScaling={false}
53
98
  >
54
- {i + 1}
99
+ ···
55
100
  </MyazaText>
56
- )}
57
- </View>
58
- {i < stepCount - 1 ? (
101
+ </View>
102
+ ) : (
103
+ // Positioning context for the badge, which straddles the circle's
104
+ // edge. Nothing here clips, so the overhang renders.
105
+ <View style={{ width: size, height: size }} importantForAccessibility="no">
106
+ <View
107
+ style={{
108
+ width: size,
109
+ height: size,
110
+ borderRadius: size / 2,
111
+ alignItems: 'center',
112
+ justifyContent: 'center',
113
+ backgroundColor: filled ? colors.primary : 'transparent',
114
+ borderWidth: 1.5,
115
+ borderColor: filled ? colors.primary : colors.primary200,
116
+ }}
117
+ >
118
+ {/* The NUMBER stays, completed or not. A check alone says a
119
+ step is done but not WHICH step — and once the row is
120
+ windowed ("1 ··· 5 6 7 ··· 10") that is precisely what the
121
+ numbers are there to answer. */}
122
+ <MyazaText
123
+ variant="bodySmall"
124
+ color={filled ? '#FFFFFF' : colors.textMuted}
125
+ style={{ fontSize: 12, fontWeight: '700' }}
126
+ // The circle already scales with the system text size, so
127
+ // scaling the glyph again would overflow it.
128
+ allowFontScaling={false}
129
+ >
130
+ {slot + 1}
131
+ </MyazaText>
132
+ </View>
133
+
134
+ {/* Completion rides as a badge tucked onto the circle's corner.
135
+ The ring is WHITE — the same colour as the number inside the
136
+ circle — because the badge sits on the circle, not on the
137
+ page. Ringing it in the page background (which the chip
138
+ portrait's CheckBadge does, correctly, because it sits on a
139
+ photo) painted a near-black blob around the check on the dark
140
+ theme, where background is #040218. */}
141
+ {completed ? (
142
+ <View
143
+ style={{
144
+ position: 'absolute',
145
+ top: -3,
146
+ right: -2,
147
+ width: badge,
148
+ height: badge,
149
+ borderRadius: badge / 2,
150
+ backgroundColor: colors.success,
151
+ // 1px, not 1.5: on a 13px badge a 1.5px ring eats nearly a
152
+ // quarter of the diameter and reads as a white outline
153
+ // rather than a hairline separating badge from circle.
154
+ borderWidth: 1,
155
+ borderColor: '#FFFFFF',
156
+ alignItems: 'center',
157
+ justifyContent: 'center',
158
+ }}
159
+ >
160
+ <Icon name="check" size={Math.round(badge * 0.6)} color="#FFFFFF" />
161
+ </View>
162
+ ) : null}
163
+ </View>
164
+ )}
165
+ {position < slots.length - 1 ? (
59
166
  <View
60
167
  style={{
61
168
  flex: 1,
169
+ // Floor, so an unforeseen overflow degrades to a visibly short
170
+ // connector rather than a row of circles with no chain at all.
171
+ minWidth: 6,
62
172
  height: 2,
173
+ // UNIFORM, deliberately: fitStepCircles budgets 3+3 per
174
+ // connector, so widening this for completed steps made the row
175
+ // need more space than was reserved. The badge is tucked in
176
+ // far enough (right: -2) not to need the extra.
63
177
  marginHorizontal: 3,
64
178
  borderRadius: 1,
65
179
  backgroundColor: completed ? colors.primary : colors.primary200,
@@ -1,6 +1,10 @@
1
+ import type { TextStyle } from 'react-native';
1
2
  import { useFonts } from 'expo-font';
2
3
  import { SpaceGrotesk_500Medium, SpaceGrotesk_600SemiBold, SpaceGrotesk_700Bold } from '@expo-google-fonts/space-grotesk';
3
4
  import { Karla_400Regular, Karla_500Medium, Karla_600SemiBold, Karla_700Bold } from '@expo-google-fonts/karla';
5
+ import { fontFamilyFor } from '../config/font-resolve';
6
+ import { useTheme } from './theme-provider';
7
+
4
8
  export { fontFamilyFor, markFamilyName, brandFamilyName, BRAND_WEIGHTS } from '../config/font-resolve';
5
9
 
6
10
 
@@ -30,3 +34,22 @@ export function useMyazaFonts(): boolean {
30
34
  * Returns `undefined` until fonts are loaded so text falls back to the system
31
35
  * font + `fontWeight` without triggering an "unrecognized font" warning.
32
36
  */
37
+
38
+ /**
39
+ * The body font family for a TEXT INPUT, at the given weight.
40
+ *
41
+ * `TextInput` does not inherit `fontFamily` from any ancestor the way web
42
+ * inputs inherit from a stylesheet — RN resolves it per-element — so an input
43
+ * that never sets one renders in the system face while every `MyazaText`
44
+ * beside it renders in the brand's. The placeholder follows the input's own
45
+ * family too, which is why it looked wrong as well.
46
+ *
47
+ * Goes through the same resolver as MyazaText so an org's uploaded brand font
48
+ * reaches inputs, not just text. Returns undefined until fonts load, which is
49
+ * the caller's cue to leave `fontFamily` unset rather than name a family the
50
+ * platform has not registered.
51
+ */
52
+ export function useInputFontFamily(weight: TextStyle['fontWeight'] = '400'): string | undefined {
53
+ const { fontsLoaded, brandFonts } = useTheme();
54
+ return fontFamilyFor(false, weight, fontsLoaded, brandFonts);
55
+ }
@@ -39,17 +39,24 @@ export function currencyKeyFor(field: QuestionnaireField): string {
39
39
  return `${field.key}_currency`;
40
40
  }
41
41
 
42
+ /** The companion key holding what an "Other" choice actually was. */
43
+ export function otherKeyFor(field: QuestionnaireField): string {
44
+ return `${field.key}_other`;
45
+ }
46
+
42
47
  /**
43
- * Every answer key a definition can produce — money questions contribute two.
44
- * The server uses the same expansion to cross-check decision-graph references,
45
- * so anything reasoning about "the keys this questionnaire yields" must use it
46
- * rather than mapping over `fields`.
48
+ * Every answer key a definition can produce — money questions contribute two,
49
+ * and so does a choice question offering an "Other". The server uses the same
50
+ * expansion to cross-check decision-graph references, so anything reasoning
51
+ * about "the keys this questionnaire yields" must use it rather than mapping
52
+ * over `fields`.
47
53
  */
48
54
  export function questionnaireAnswerKeys(questionnaire: QuestionnaireConfig | undefined): string[] {
49
55
  const keys: string[] = [];
50
56
  for (const field of questionnaire?.fields ?? []) {
51
57
  keys.push(field.key);
52
58
  if (field.type === 'money') keys.push(currencyKeyFor(field));
59
+ if ((field.options ?? []).some((o) => o.requiresDetail)) keys.push(otherKeyFor(field));
53
60
  }
54
61
  return keys;
55
62
  }
@@ -78,6 +85,23 @@ export function validateQuestionnaire(
78
85
  continue;
79
86
  }
80
87
 
88
+ // An "Other" choice obliges a description, whether or not the question
89
+ // itself is required: an unexplained "Other" is the answer that most needs
90
+ // explaining. Checked before the per-type rules so it applies to select and
91
+ // multiselect alike.
92
+ const detailOption = (field.options ?? []).find(
93
+ (o) =>
94
+ o.requiresDetail &&
95
+ (Array.isArray(value) ? (value as string[]).includes(o.value) : value === o.value),
96
+ );
97
+ if (detailOption) {
98
+ const detail = answers[`${field.key}_other`];
99
+ if (typeof detail !== 'string' || !detail.trim()) {
100
+ errors[field.key] = `Tell us more about "${detailOption.label}".`;
101
+ continue;
102
+ }
103
+ }
104
+
81
105
  if (field.type === 'number' || field.type === 'money') {
82
106
  const num = Number(value);
83
107
  if (!Number.isFinite(num) || (field.type === 'money' && num < 0)) {
@@ -147,6 +171,23 @@ export function questionnairePayload(
147
171
  }
148
172
 
149
173
  payload[field.key] = value as QuestionnaireAnswerValue;
174
+
175
+ // The description behind an "Other" choice. Carried explicitly for the same
176
+ // reason `_currency` is: this builds the request from the FIELD LIST, so a
177
+ // companion answer that is not named here is silently dropped — the user
178
+ // types it, the client accepts it, and the server then rejects the
179
+ // submission for a missing detail it was never sent.
180
+ const detailOption = (field.options ?? []).find(
181
+ (o) =>
182
+ o.requiresDetail &&
183
+ (Array.isArray(value) ? (value as string[]).includes(o.value) : value === o.value),
184
+ );
185
+ if (detailOption) {
186
+ const detail = answers[otherKeyFor(field)];
187
+ if (typeof detail === 'string' && detail.trim() !== '') {
188
+ payload[otherKeyFor(field)] = detail.trim();
189
+ }
190
+ }
150
191
  }
151
192
 
152
193
  return payload;
@@ -30,6 +30,7 @@ export const WORKFLOW_KEYS = [
30
30
  'requireMobileDevice',
31
31
  'voiceGuidance',
32
32
  'showThemeToggle',
33
+ 'progressStyle',
33
34
  'disableClose',
34
35
  'appearance',
35
36
  'consent',
@@ -99,7 +99,7 @@ export function deriveKey(
99
99
  }
100
100
 
101
101
  // The MRZ-derived key lives in ./mrzKey; re-exported so callers keep one import.
102
- export { keySeed, mrzKeySeedInput, type MrzKeyFields } from './mrzKey';
102
+ export { keySeed, paceKeySeed, mrzKeySeedInput, type MrzKeyFields } from './mrzKey';
103
103
 
104
104
  /** The pair of session keys derived from a seed. */
105
105
  export interface SessionKeys {
@@ -0,0 +1,55 @@
1
+ import { fromBase64 } from './bytes';
2
+ import { findTlvDeep } from './der';
3
+ import { parseMrz, type MrzScan } from '../mrz/parse';
4
+
5
+ // ---------------------------------------------------------------------------
6
+ // THE CHIP'S OWN MRZ.
7
+ //
8
+ // DG1 is the machine-readable zone exactly as the ISSUING STATE wrote it, and
9
+ // passive authentication hashes it against the signed security object. It is
10
+ // the strongest copy of the holder's details that exists on the document.
11
+ //
12
+ // The camera scan is a GUESS at the same characters, and it is not a safe
13
+ // substitute for display. Measured on a real Nigerian passport read on a Galaxy
14
+ // S24: the on-device recogniser read the first '<' of the '<<' surname
15
+ // separator as 'K', so `INGWE<<RICHARD<UNIMKE` arrived as `INGWEK<RICHARD…`.
16
+ // The split then never fired at the surname boundary and landed in the trailing
17
+ // filler instead, yielding lastName "INGWEK RICHARD UNIMKE" and firstName
18
+ // "KKKK" — displayed under a caption promising we had read the secure chip.
19
+ //
20
+ // TD3 carries NO check digit over the name field (only line 2 is protected), so
21
+ // that corruption passes `parseMrz` validation silently. There is no way to
22
+ // detect it from the scan alone, which is exactly why the chip has to be the
23
+ // source once we hold it.
24
+ //
25
+ // Reuses `parseMrz` rather than parsing here: DG1's value IS the continuous
26
+ // 88/90-character string that function already takes, and a second MRZ parser
27
+ // would be one more thing to drift.
28
+ // ---------------------------------------------------------------------------
29
+
30
+ /** DG1's MRZ lives under tag 5F1F, inside the 0x61 template. */
31
+ const TAG_MRZ = 0x5f1f;
32
+
33
+ /**
34
+ * The MRZ read off the chip, or null when DG1 is absent, malformed, or not a
35
+ * size `parseMrz` recognises.
36
+ *
37
+ * Null is a normal outcome the caller falls back from, never an error: the
38
+ * read itself already succeeded and its bytes still go to the server, which
39
+ * parses DG1 authoritatively regardless of what this returns.
40
+ */
41
+ export function parseDg1(dg1Base64: string | undefined | null): MrzScan | null {
42
+ if (!dg1Base64) return null;
43
+ try {
44
+ const bytes = fromBase64(dg1Base64);
45
+ const mrz = findTlvDeep(bytes, TAG_MRZ);
46
+ if (!mrz) return null;
47
+ let text = '';
48
+ for (const byte of mrz.value) text += String.fromCharCode(byte);
49
+ return parseMrz(text);
50
+ } catch {
51
+ // Display-path only. A malformed DG1 must never take down the success
52
+ // screen of a read that otherwise worked.
53
+ return null;
54
+ }
55
+ }
@@ -0,0 +1,142 @@
1
+ // ---------------------------------------------------------------------------
2
+ // Standardised elliptic-curve domain parameters (ICAO 9303 / BSI TR-03110).
3
+ //
4
+ // A chip names its PACE curve by a standardised parameter id rather than
5
+ // sending the parameters, so both sides must agree on this table. Every curve
6
+ // here is a prime-field short-Weierstrass curve (y² = x³ + ax + b), so one set
7
+ // of point arithmetic (ec.ts) covers all of them.
8
+ //
9
+ // The constants are transcribed from the authoritative sources — NIST FIPS
10
+ // 186-4 for the secp/P- curves and RFC 5639 for the brainpool curves — and are
11
+ // verified in ec.test.ts, which asserts G lies on each curve and that n·G is
12
+ // the point at infinity. A transcription typo fails that test rather than a
13
+ // passport.
14
+ //
15
+ // The six curves below are the ones issued eMRTDs actually use. The other
16
+ // standardised ids (the 192/224-bit curves, DH groups) are left unmapped: an
17
+ // unknown id resolves to null, which the caller treats exactly like a chip that
18
+ // does not offer PACE — it reads over BAC. So the coverage gap can only ever
19
+ // cost a fallback, never a failed read.
20
+ // ---------------------------------------------------------------------------
21
+
22
+ /** A prime-field short-Weierstrass curve and its generator. */
23
+ export interface EcCurve {
24
+ readonly name: string;
25
+ /** Field prime. */
26
+ readonly p: bigint;
27
+ readonly a: bigint;
28
+ readonly b: bigint;
29
+ /** Generator coordinates. */
30
+ readonly gx: bigint;
31
+ readonly gy: bigint;
32
+ /** Group order. */
33
+ readonly n: bigint;
34
+ /** Field width in bytes — the fixed length x/y coordinates encode to. */
35
+ readonly byteLen: number;
36
+ }
37
+
38
+ const hex = (s: string): bigint => BigInt(`0x${s.replace(/\s+/g, '')}`);
39
+
40
+ function curve(
41
+ name: string,
42
+ byteLen: number,
43
+ p: string,
44
+ a: string,
45
+ b: string,
46
+ gx: string,
47
+ gy: string,
48
+ n: string,
49
+ ): EcCurve {
50
+ return { name, byteLen, p: hex(p), a: hex(a), b: hex(b), gx: hex(gx), gy: hex(gy), n: hex(n) };
51
+ }
52
+
53
+ const secp256r1 = curve(
54
+ 'secp256r1',
55
+ 32,
56
+ 'FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFF',
57
+ 'FFFFFFFF00000001000000000000000000000000FFFFFFFFFFFFFFFFFFFFFFFC',
58
+ '5AC635D8AA3A93E7B3EBBD55769886BC651D06B0CC53B0F63BCE3C3E27D2604B',
59
+ '6B17D1F2E12C4247F8BCE6E563A440F277037D812DEB33A0F4A13945D898C296',
60
+ '4FE342E2FE1A7F9B8EE7EB4A7C0F9E162BCE33576B315ECECBB6406837BF51F5',
61
+ 'FFFFFFFF00000000FFFFFFFFFFFFFFFFBCE6FAADA7179E84F3B9CAC2FC632551',
62
+ );
63
+
64
+ const brainpoolP256r1 = curve(
65
+ 'brainpoolP256r1',
66
+ 32,
67
+ 'A9FB57DBA1EEA9BC3E660A909D838D726E3BF623D52620282013481D1F6E5377',
68
+ '7D5A0975FC2C3057EEF67530417AFFE7FB8055C126DC5C6CE94A4B44F330B5D9',
69
+ '26DC5C6CE94A4B44F330B5D9BBD77CBF958416295CF7E1CE6BCCDC18FF8C07B6',
70
+ '8BD2AEB9CB7E57CB2C4B482FFC81B7AFB9DE27E1E3BD23C23A4453BD9ACE3262',
71
+ '547EF835C3DAC4FD97F8461A14611DC9C27745132DED8E545C1D54C72F046997',
72
+ 'A9FB57DBA1EEA9BC3E660A909D838D718C397AA3B561A6F7901E0E82974856A7',
73
+ );
74
+
75
+ const secp384r1 = curve(
76
+ 'secp384r1',
77
+ 48,
78
+ 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFF',
79
+ 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEFFFFFFFF0000000000000000FFFFFFFC',
80
+ 'B3312FA7E23EE7E4988E056BE3F82D19181D9C6EFE8141120314088F5013875AC656398D8A2ED19D2A85C8EDD3EC2AEF',
81
+ 'AA87CA22BE8B05378EB1C71EF320AD746E1D3B628BA79B9859F741E082542A385502F25DBF55296C3A545E3872760AB7',
82
+ '3617DE4A96262C6F5D9E98BF9292DC29F8F41DBD289A147CE9DA3113B5F0B8C00A60B1CE1D7E819D7A431D7C90EA0E5F',
83
+ 'FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC7634D81F4372DDF581A0DB248B0A77AECEC196ACCC52973',
84
+ );
85
+
86
+ const brainpoolP384r1 = curve(
87
+ 'brainpoolP384r1',
88
+ 48,
89
+ '8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B412B1DA197FB71123ACD3A729901D1A71874700133107EC53',
90
+ '7BC382C63D8C150C3C72080ACE05AFA0C2BEA28E4FB22787139165EFBA91F90F8AA5814A503AD4EB04A8C7DD22CE2826',
91
+ '04A8C7DD22CE28268B39B55416F0447C2FB77DE107DCD2A62E880EA53EEB62D57CB4390295DBC9943AB78696FA504C11',
92
+ '1D1C64F068CF45FFA2A63A81B7C13F6B8847A3E77EF14FE3DB7FCAFE0CBD10E8E826E03436D646AAEF87B2E247D4AF1E',
93
+ '8ABE1D7520F9C2A45CB1EB8E95CFD55262B70B29FEEC5864E19C054FF99129280E4646217791811142820341263C5315',
94
+ '8CB91E82A3386D280F5D6F7E50E641DF152F7109ED5456B31F166E6CAC0425A7CF3AB6AF6B7FC3103B883202E9046565',
95
+ );
96
+
97
+ const brainpoolP512r1 = curve(
98
+ 'brainpoolP512r1',
99
+ 64,
100
+ 'AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA703308717D4D9B009BC66842AECDA12AE6A380E62881FF2F2D82C68528AA6056583A48F3',
101
+ '7830A3318B603B89E2327145AC234CC594CBDD8D3DF91610A83441CAEA9863BC2DED5D5AA8253AA10A2EF1C98B9AC8B57F1117A72BF2C7B9E7C1AC4D77FC94CA',
102
+ '3DF91610A83441CAEA9863BC2DED5D5AA8253AA10A2EF1C98B9AC8B57F1117A72BF2C7B9E7C1AC4D77FC94CADC083E67984050B75EBAE5DD2809BD638016F723',
103
+ '81AEE4BDD82ED9645A21322E9C4C6A9385ED9F70B5D916C1B43B62EEF4D0098EFF3B1F78E2D0D48D50D1687B93B97D5F7C6D5047406A5E688B352209BCB9F822',
104
+ '7DDE385D566332ECC0EABFA9CF7822FDF209F70024A57B1AA000C55B881F8111B2DCDE494A5F485E5BCA4BD88A2763AED1CA2B2FA8F0540678CD1E0F3AD80892',
105
+ 'AADD9DB8DBE9C48B3FD4E6AE33C9FC07CB308DB3B3C9D20ED6639CCA70330870553E5C414CA92619418661197FAC10471DB1D381085DDADDB58796829CA90069',
106
+ );
107
+
108
+ const secp521r1 = curve(
109
+ 'secp521r1',
110
+ 66,
111
+ '01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF',
112
+ '01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFC',
113
+ '0051953EB9618E1C9A1F929A21A0B68540EEA2DA725B99B315F3B8B489918EF109E156193951EC7E937B1652C0BD3BB1BF073573DF883D2C34F1EF451FD46B503F00',
114
+ '00C6858E06B70404E9CD9E3ECB662395B4429C648139053FB521F828AF606B4D3DBAA14B5E77EFE75928FE1DC127A2FFA8DE3348B3C1856A429BF97E7E31C2E5BD66',
115
+ '011839296A789A3BC0045C8A5FB42C7D1BD998F54449579B446817AFBD17273E662C97EE72995EF42640C550B9013FAD0761353C7086A272C24088BE94769FD16650',
116
+ '01FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFA51868783BF2F966B7FCC0148F709A5D03BB5C9B8899C47AEBB6FB71E91386409',
117
+ );
118
+
119
+ /** ICAO standardised parameter id → curve, for the curves this build runs. */
120
+ const BY_ID: Record<number, EcCurve> = {
121
+ 12: secp256r1,
122
+ 13: brainpoolP256r1,
123
+ 15: secp384r1,
124
+ 16: brainpoolP384r1,
125
+ 17: brainpoolP512r1,
126
+ 18: secp521r1,
127
+ };
128
+
129
+ /** The curve for a standardised parameter id, or null when unmapped. */
130
+ export function curveForParameterId(id: number): EcCurve | null {
131
+ return BY_ID[id] ?? null;
132
+ }
133
+
134
+ /** Exposed for tests — every curve this build claims to support. */
135
+ export const ALL_CURVES: readonly EcCurve[] = [
136
+ secp256r1,
137
+ brainpoolP256r1,
138
+ secp384r1,
139
+ brainpoolP384r1,
140
+ brainpoolP512r1,
141
+ secp521r1,
142
+ ];