@incode-sdks/incode-integrate 0.1.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/README.md +33 -0
- package/agent.js +1317 -0
- package/executor.js +301 -0
- package/package.json +36 -0
- package/scanner.js +120 -0
package/agent.js
ADDED
|
@@ -0,0 +1,1317 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
const fs = require("fs");
|
|
4
|
+
const path = require("path");
|
|
5
|
+
const readline = require("readline");
|
|
6
|
+
const { applyPlan } = require("./executor");
|
|
7
|
+
const { SDK_PACKAGE, scanProject } = require("./scanner");
|
|
8
|
+
|
|
9
|
+
const DEFAULT_SDK_VERSION = "9.14.0";
|
|
10
|
+
const VERSION_CHOICES = ["9.14.0", "9.13.0"];
|
|
11
|
+
const VARIANT_CHOICES = [
|
|
12
|
+
{ label: "standard", suffix: "" },
|
|
13
|
+
{ label: "streaming", suffix: "vc" },
|
|
14
|
+
{ label: "face-login", suffix: "l" },
|
|
15
|
+
{ label: "nfc", suffix: "nfc" },
|
|
16
|
+
{ label: "e2ee", suffix: "e2ee" },
|
|
17
|
+
];
|
|
18
|
+
const POD_SOURCE_CHOICES = ["ssh", "https"];
|
|
19
|
+
const CONFIG_MODE_CHOICES = ["online", "manual"];
|
|
20
|
+
const IDV_CHOICES = ["standard", "standard-nfc", "standard-recording"];
|
|
21
|
+
const FLOW_HANDLING_CHOICES = ["end-to-end", "step-by-step"];
|
|
22
|
+
const API_URLS = {
|
|
23
|
+
demo: {
|
|
24
|
+
label: "Demo",
|
|
25
|
+
url: "https://demo-api.incodesmile.com",
|
|
26
|
+
},
|
|
27
|
+
production: {
|
|
28
|
+
label: "Production",
|
|
29
|
+
url: "https://saas-api.incodesmile.com",
|
|
30
|
+
},
|
|
31
|
+
};
|
|
32
|
+
const DASHBOARD_URL = "https://dashboard.incodesmile.com";
|
|
33
|
+
const LOCAL_SECRET_FILES = [".incode.local.env", ".env.local"];
|
|
34
|
+
|
|
35
|
+
const IDV_PRESETS = {
|
|
36
|
+
standard: {
|
|
37
|
+
description: "Running ID, selfie, and face-match modules...",
|
|
38
|
+
label: "Standard KYC",
|
|
39
|
+
modules: [{ module: "IdScan" }, { module: "SelfieScan" }, { module: "FaceMatch" }],
|
|
40
|
+
sections: [
|
|
41
|
+
{ sectionTag: "id", flowConfig: [{ module: "IdScan" }] },
|
|
42
|
+
{ sectionTag: "selfie", flowConfig: [{ module: "SelfieScan" }, { module: "FaceMatch" }] },
|
|
43
|
+
],
|
|
44
|
+
listeners: [
|
|
45
|
+
["IdScanFront", "ID front captured"],
|
|
46
|
+
["IdScanBack", "ID back captured"],
|
|
47
|
+
["ProcessId", "ID processed"],
|
|
48
|
+
["SelfieScan", "Selfie captured"],
|
|
49
|
+
["FaceMatch", "Face match completed"],
|
|
50
|
+
],
|
|
51
|
+
variant: "standard",
|
|
52
|
+
},
|
|
53
|
+
"standard-nfc": {
|
|
54
|
+
description: "Running ID, NFC, selfie, and face-match modules...",
|
|
55
|
+
label: "Standard KYC + NFC",
|
|
56
|
+
modules: [
|
|
57
|
+
{ module: "IdScan" },
|
|
58
|
+
{ module: "NFCScan", processNFCData: true },
|
|
59
|
+
{ module: "SelfieScan" },
|
|
60
|
+
{ module: "FaceMatch", matchType: "nfc3Way" },
|
|
61
|
+
],
|
|
62
|
+
sections: [
|
|
63
|
+
{ sectionTag: "id", flowConfig: [{ module: "IdScan" }] },
|
|
64
|
+
{ sectionTag: "nfc", flowConfig: [{ module: "NFCScan", processNFCData: true }] },
|
|
65
|
+
{
|
|
66
|
+
sectionTag: "selfie",
|
|
67
|
+
flowConfig: [{ module: "SelfieScan" }, { module: "FaceMatch", matchType: "nfc3Way" }],
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
listeners: [
|
|
71
|
+
["IdScanFront", "ID front captured"],
|
|
72
|
+
["IdScanBack", "ID back captured"],
|
|
73
|
+
["ProcessId", "ID processed"],
|
|
74
|
+
["NFCScan", "NFC scan completed"],
|
|
75
|
+
["SelfieScan", "Selfie captured"],
|
|
76
|
+
["FaceMatch", "Face match completed"],
|
|
77
|
+
],
|
|
78
|
+
variant: "nfc",
|
|
79
|
+
},
|
|
80
|
+
"standard-recording": {
|
|
81
|
+
description: "Running ID, selfie, and face-match modules with session recording...",
|
|
82
|
+
label: "Standard KYC + Session Recording",
|
|
83
|
+
modules: [
|
|
84
|
+
{ module: "IdScan", streamFrames: true },
|
|
85
|
+
{ module: "SelfieScan", streamFrames: true },
|
|
86
|
+
{ module: "FaceMatch" },
|
|
87
|
+
],
|
|
88
|
+
recordSessionConfig: { forcePermissions: false, recordSession: true },
|
|
89
|
+
sections: [
|
|
90
|
+
{ sectionTag: "id", flowConfig: [{ module: "IdScan", streamFrames: true }] },
|
|
91
|
+
{
|
|
92
|
+
sectionTag: "selfie",
|
|
93
|
+
flowConfig: [{ module: "SelfieScan", streamFrames: true }, { module: "FaceMatch" }],
|
|
94
|
+
},
|
|
95
|
+
],
|
|
96
|
+
listeners: [
|
|
97
|
+
["IdScanFront", "ID front captured"],
|
|
98
|
+
["IdScanBack", "ID back captured"],
|
|
99
|
+
["ProcessId", "ID processed"],
|
|
100
|
+
["SelfieScan", "Selfie captured"],
|
|
101
|
+
["FaceMatch", "Face match completed"],
|
|
102
|
+
],
|
|
103
|
+
variant: "streaming",
|
|
104
|
+
videoStreaming: true,
|
|
105
|
+
},
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
function parseArgs(argv) {
|
|
109
|
+
const valueFor = name => {
|
|
110
|
+
const inline = argv.find(arg => arg.startsWith(`--${name}=`));
|
|
111
|
+
if (inline) {
|
|
112
|
+
return inline.slice(name.length + 3);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const index = argv.indexOf(`--${name}`);
|
|
116
|
+
return index >= 0 ? argv[index + 1] : "";
|
|
117
|
+
};
|
|
118
|
+
const optionalBoolean = name => {
|
|
119
|
+
if (argv.includes(`--${name}`)) {
|
|
120
|
+
return true;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (argv.includes(`--no-${name}`)) {
|
|
124
|
+
return false;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const value = valueFor(name);
|
|
128
|
+
if (!value) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return ["1", "true", "yes", "y"].includes(value.toLowerCase());
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
apiKey: valueFor("api-key"),
|
|
137
|
+
apiUrl: valueFor("api-url"),
|
|
138
|
+
apply: argv.includes("--apply"),
|
|
139
|
+
backendSessions: valueFor("backend-sessions"),
|
|
140
|
+
configurationId: valueFor("configuration-id"),
|
|
141
|
+
dashboardUrl: valueFor("dashboard-url"),
|
|
142
|
+
defaults: argv.includes("--yes") || argv.includes("--defaults") || argv.includes("--no-interactive"),
|
|
143
|
+
dynamicLocalization: optionalBoolean("dynamic-localization"),
|
|
144
|
+
flow: valueFor("flow"),
|
|
145
|
+
flowHandling: valueFor("flow-handling"),
|
|
146
|
+
githubToken: valueFor("github-token"),
|
|
147
|
+
githubUsername: valueFor("github-username"),
|
|
148
|
+
idv: valueFor("idv"),
|
|
149
|
+
npmToken: valueFor("npm-token"),
|
|
150
|
+
podSource: valueFor("pod-source"),
|
|
151
|
+
sdkConfig: valueFor("sdk-config") || valueFor("config-mode"),
|
|
152
|
+
testMode: optionalBoolean("test-mode"),
|
|
153
|
+
version: valueFor("version"),
|
|
154
|
+
videoStreaming: optionalBoolean("video-streaming"),
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function normalizeVersion(version) {
|
|
159
|
+
const trimmed = String(version || "").trim();
|
|
160
|
+
if (/^\d+\.\d+$/.test(trimmed)) {
|
|
161
|
+
return `${trimmed}.0`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
return trimmed || DEFAULT_SDK_VERSION;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function normalizeVariant(variant) {
|
|
168
|
+
const normalized = String(variant || "").trim().toLowerCase();
|
|
169
|
+
const aliases = {
|
|
170
|
+
"": "standard",
|
|
171
|
+
default: "standard",
|
|
172
|
+
face: "face-login",
|
|
173
|
+
facelogin: "face-login",
|
|
174
|
+
login: "face-login",
|
|
175
|
+
l: "face-login",
|
|
176
|
+
nfc: "nfc",
|
|
177
|
+
standard: "standard",
|
|
178
|
+
stream: "streaming",
|
|
179
|
+
streaming: "streaming",
|
|
180
|
+
vc: "streaming",
|
|
181
|
+
video: "streaming",
|
|
182
|
+
recording: "streaming",
|
|
183
|
+
e2ee: "e2ee",
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
return aliases[normalized] || normalized;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function variantSuffix(variant) {
|
|
190
|
+
const selected = VARIANT_CHOICES.find(choice => choice.label === normalizeVariant(variant));
|
|
191
|
+
if (!selected) {
|
|
192
|
+
throw new Error(`Unknown SDK variant "${variant}". Use standard, streaming, face-login, nfc, or e2ee.`);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
return selected.suffix;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
function dependencyForRegistry(version, variant) {
|
|
199
|
+
const normalizedVersion = normalizeVersion(version);
|
|
200
|
+
const suffix = variantSuffix(variant);
|
|
201
|
+
return suffix ? `${normalizedVersion}-${suffix}` : normalizedVersion;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function normalizeChoice(value, choices, fallback) {
|
|
205
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
206
|
+
return choices.includes(normalized) ? normalized : fallback;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function normalizeConfigMode(value, fallback = "manual") {
|
|
210
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
211
|
+
if (["online", "dashboard", "remote"].includes(normalized)) {
|
|
212
|
+
return "online";
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (["manual", "code", "in-code", "incode", "local"].includes(normalized)) {
|
|
216
|
+
return "manual";
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
return fallback;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
function normalizeFlowHandling(value, fallback = "end-to-end") {
|
|
223
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
224
|
+
if (["section", "sections", "step", "step-by-step", "sectioned"].includes(normalized)) {
|
|
225
|
+
return "step-by-step";
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
if (["e2e", "end", "end-to-end", "startonboarding"].includes(normalized)) {
|
|
229
|
+
return "end-to-end";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
return fallback;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function normalizeIdv(value, fallback = "standard") {
|
|
236
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
237
|
+
const aliases = {
|
|
238
|
+
"": fallback,
|
|
239
|
+
basic: "standard",
|
|
240
|
+
"basic-kyc": "standard",
|
|
241
|
+
document: "standard",
|
|
242
|
+
"document-only": "standard",
|
|
243
|
+
face: "standard",
|
|
244
|
+
kyc: "standard",
|
|
245
|
+
recording: "standard-recording",
|
|
246
|
+
"session-recording": "standard-recording",
|
|
247
|
+
"standard+recording": "standard-recording",
|
|
248
|
+
"standard-recording": "standard-recording",
|
|
249
|
+
streaming: "standard-recording",
|
|
250
|
+
video: "standard-recording",
|
|
251
|
+
nfc: "standard-nfc",
|
|
252
|
+
"standard+nfc": "standard-nfc",
|
|
253
|
+
"standard-nfc": "standard-nfc",
|
|
254
|
+
standard: "standard",
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
return aliases[normalized] || fallback;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
function idvForVariant(variant, fallback = "standard") {
|
|
261
|
+
const normalized = normalizeVariant(variant);
|
|
262
|
+
|
|
263
|
+
if (normalized === "nfc") {
|
|
264
|
+
return "standard-nfc";
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (normalized === "streaming") {
|
|
268
|
+
return "standard-recording";
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
return fallback;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
function variantForIdv(idv) {
|
|
275
|
+
return (IDV_PRESETS[idv] || IDV_PRESETS.standard).variant;
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function normalizeBackendSessions(value, fallback = true) {
|
|
279
|
+
const normalized = String(value || "").trim().toLowerCase();
|
|
280
|
+
if (["1", "backend", "no-api-key", "true", "yes", "y"].includes(normalized)) {
|
|
281
|
+
return true;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (["0", "api-key", "client", "false", "no", "n"].includes(normalized)) {
|
|
285
|
+
return false;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
return fallback;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
function escapeEnvValue(value) {
|
|
292
|
+
return String(value)
|
|
293
|
+
.replace(/\\/g, "\\\\")
|
|
294
|
+
.replace(/"/g, '\\"')
|
|
295
|
+
.replace(/\$/g, "\\$")
|
|
296
|
+
.replace(/`/g, "\\`");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function unquoteEnvValue(value) {
|
|
300
|
+
const trimmed = String(value || "").trim();
|
|
301
|
+
|
|
302
|
+
if (
|
|
303
|
+
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
|
304
|
+
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
|
305
|
+
) {
|
|
306
|
+
return trimmed.slice(1, -1);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return trimmed;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function isUsableSecret(value) {
|
|
313
|
+
const trimmed = String(value || "").trim();
|
|
314
|
+
return Boolean(trimmed) && !/^<.*>$/.test(trimmed);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function parseEnvFile(content) {
|
|
318
|
+
const values = {};
|
|
319
|
+
|
|
320
|
+
for (const line of content.split("\n")) {
|
|
321
|
+
const trimmed = line.trim();
|
|
322
|
+
if (!trimmed || trimmed.startsWith("#")) {
|
|
323
|
+
continue;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
const assignment = trimmed.startsWith("export ")
|
|
327
|
+
? trimmed.slice("export ".length).trim()
|
|
328
|
+
: trimmed;
|
|
329
|
+
const equalsIndex = assignment.indexOf("=");
|
|
330
|
+
|
|
331
|
+
if (equalsIndex <= 0) {
|
|
332
|
+
continue;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const key = assignment.slice(0, equalsIndex).trim();
|
|
336
|
+
const value = unquoteEnvValue(assignment.slice(equalsIndex + 1));
|
|
337
|
+
|
|
338
|
+
if (/^[A-Z0-9_]+$/.test(key) && isUsableSecret(value)) {
|
|
339
|
+
values[key] = value;
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
return values;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function readLocalSecrets(root) {
|
|
347
|
+
return LOCAL_SECRET_FILES.reduce((secrets, file) => {
|
|
348
|
+
const filePath = path.join(root, file);
|
|
349
|
+
if (!fs.existsSync(filePath)) {
|
|
350
|
+
return secrets;
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
return {
|
|
354
|
+
...secrets,
|
|
355
|
+
...parseEnvFile(fs.readFileSync(filePath, "utf8")),
|
|
356
|
+
};
|
|
357
|
+
}, {});
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function withHttps(url) {
|
|
361
|
+
const trimmed = String(url || "").trim();
|
|
362
|
+
if (!trimmed) {
|
|
363
|
+
return "";
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
return /^https?:\/\//i.test(trimmed) ? trimmed : `https://${trimmed}`;
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
function normalizeApiUrl(value, fallback = API_URLS.demo.url) {
|
|
370
|
+
const trimmed = String(value || "").trim();
|
|
371
|
+
const normalized = trimmed.toLowerCase();
|
|
372
|
+
|
|
373
|
+
if (!trimmed) {
|
|
374
|
+
return fallback;
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (["demo", "demo-api", "sandbox"].includes(normalized)) {
|
|
378
|
+
return API_URLS.demo.url;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
if (["prod", "production", "saas", "saas-api"].includes(normalized)) {
|
|
382
|
+
return API_URLS.production.url;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
return withHttps(trimmed);
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
function labelForApiUrl(url) {
|
|
389
|
+
if (url === API_URLS.demo.url) {
|
|
390
|
+
return API_URLS.demo.label;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
if (url === API_URLS.production.url) {
|
|
394
|
+
return API_URLS.production.label;
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
return "Custom";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
function podSourceForMode(mode) {
|
|
401
|
+
if (mode === "https") {
|
|
402
|
+
return "source 'https://github.com/Incode-Technologies-Example-Repos/IncdDistributionPodspecs.git'";
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return "source 'git@github.com:Incode-Technologies-Example-Repos/IncdDistributionPodspecs.git'";
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
function parseCurrentDependency(dependency) {
|
|
409
|
+
if (!dependency) {
|
|
410
|
+
return {
|
|
411
|
+
source: "registry",
|
|
412
|
+
variant: "standard",
|
|
413
|
+
version: DEFAULT_SDK_VERSION,
|
|
414
|
+
};
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
if (dependency.startsWith("file:")) {
|
|
418
|
+
return {
|
|
419
|
+
source: "registry",
|
|
420
|
+
variant: "standard",
|
|
421
|
+
version: DEFAULT_SDK_VERSION,
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
const withoutRange = dependency.replace(/^[~^]/, "");
|
|
426
|
+
const match = withoutRange.match(/^(\d+\.\d+(?:\.\d+)?)(?:-(vc|l|nfc|e2ee))?$/);
|
|
427
|
+
|
|
428
|
+
if (!match) {
|
|
429
|
+
return {
|
|
430
|
+
source: "registry",
|
|
431
|
+
variant: "standard",
|
|
432
|
+
version: withoutRange,
|
|
433
|
+
};
|
|
434
|
+
}
|
|
435
|
+
|
|
436
|
+
const suffixToVariant = {
|
|
437
|
+
e2ee: "e2ee",
|
|
438
|
+
l: "face-login",
|
|
439
|
+
nfc: "nfc",
|
|
440
|
+
vc: "streaming",
|
|
441
|
+
};
|
|
442
|
+
|
|
443
|
+
return {
|
|
444
|
+
source: "registry",
|
|
445
|
+
variant: suffixToVariant[match[2]] || "standard",
|
|
446
|
+
version: normalizeVersion(match[1]),
|
|
447
|
+
};
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
function parseStringConstant(content, name) {
|
|
451
|
+
const single = content.match(new RegExp(`export const ${name}(?::[^=]+)? = '([^']*)'`));
|
|
452
|
+
if (single) {
|
|
453
|
+
return single[1];
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
const double = content.match(new RegExp(`export const ${name}(?::[^=]+)? = "([^"]*)"`));
|
|
457
|
+
return double?.[1] || "";
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function parseBooleanConstant(content, name, fallback) {
|
|
461
|
+
const match = content.match(new RegExp(`export const ${name}(?::[^=]+)? = (true|false)`));
|
|
462
|
+
return match ? match[1] === "true" : fallback;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
function extractConfigurationId(dashboardUrl) {
|
|
466
|
+
const value = String(dashboardUrl || "").trim();
|
|
467
|
+
if (!value) {
|
|
468
|
+
return "";
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
try {
|
|
472
|
+
const url = new URL(withHttps(value));
|
|
473
|
+
const fromQuery =
|
|
474
|
+
url.searchParams.get("configurationId") ||
|
|
475
|
+
url.searchParams.get("configId") ||
|
|
476
|
+
url.searchParams.get("configuration");
|
|
477
|
+
|
|
478
|
+
if (fromQuery) {
|
|
479
|
+
return fromQuery;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
const fromPath = url.pathname.match(/(?:configuration|configurations|flow|flows)\/([^/?#]+)/i);
|
|
483
|
+
return fromPath?.[1] || "";
|
|
484
|
+
} catch {
|
|
485
|
+
return "";
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
function inferVariantFromDashboardUrl(dashboardUrl) {
|
|
490
|
+
const normalized = String(dashboardUrl || "").toLowerCase();
|
|
491
|
+
|
|
492
|
+
if (normalized.includes("nfc")) {
|
|
493
|
+
return "nfc";
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
if (
|
|
497
|
+
normalized.includes("recording") ||
|
|
498
|
+
normalized.includes("session-recording") ||
|
|
499
|
+
normalized.includes("streaming") ||
|
|
500
|
+
normalized.includes("video")
|
|
501
|
+
) {
|
|
502
|
+
return "streaming";
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
return "";
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function inferIdvFromDashboardUrl(dashboardUrl) {
|
|
509
|
+
return idvForVariant(inferVariantFromDashboardUrl(dashboardUrl), "");
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
function question(rl, prompt) {
|
|
513
|
+
return new Promise(resolve => {
|
|
514
|
+
rl.question(prompt, answer => resolve(answer));
|
|
515
|
+
});
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function secretQuestion(rl, prompt) {
|
|
519
|
+
if (!rl.output) {
|
|
520
|
+
return question(rl, prompt);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
return new Promise(resolve => {
|
|
524
|
+
const output = rl.output;
|
|
525
|
+
const originalWrite = output.write;
|
|
526
|
+
|
|
527
|
+
output.write = function mutedWrite(stringToWrite, ...args) {
|
|
528
|
+
if (String(stringToWrite).includes(prompt) || stringToWrite === "\n" || stringToWrite === "\r\n") {
|
|
529
|
+
return originalWrite.call(output, stringToWrite, ...args);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
return true;
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
rl.question(prompt, answer => {
|
|
536
|
+
output.write = originalWrite;
|
|
537
|
+
console.log("");
|
|
538
|
+
resolve(answer);
|
|
539
|
+
});
|
|
540
|
+
});
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function printOptionList(options) {
|
|
544
|
+
options.forEach((option, index) => {
|
|
545
|
+
console.log(` ${index + 1}. ${option}`);
|
|
546
|
+
});
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async function chooseFromList(rl, label, choices, defaultChoice) {
|
|
550
|
+
console.log("");
|
|
551
|
+
console.log(label);
|
|
552
|
+
printOptionList(choices);
|
|
553
|
+
|
|
554
|
+
const fallbackIndex = Math.max(choices.indexOf(defaultChoice), 0);
|
|
555
|
+
const answer = await question(rl, `Choose [${fallbackIndex + 1}]: `);
|
|
556
|
+
const trimmed = answer.trim();
|
|
557
|
+
|
|
558
|
+
if (!trimmed) {
|
|
559
|
+
return choices[fallbackIndex];
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
const index = Number.parseInt(trimmed, 10);
|
|
563
|
+
if (Number.isInteger(index) && index >= 1 && index <= choices.length) {
|
|
564
|
+
return choices[index - 1];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
if (choices.includes(trimmed)) {
|
|
568
|
+
return trimmed;
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
return trimmed;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
async function chooseBoolean(rl, label, defaultValue) {
|
|
575
|
+
const defaultLabel = defaultValue ? "yes" : "no";
|
|
576
|
+
const answer = await question(rl, `${label} [${defaultLabel}]: `);
|
|
577
|
+
const normalized = answer.trim().toLowerCase();
|
|
578
|
+
|
|
579
|
+
if (!normalized) {
|
|
580
|
+
return defaultValue;
|
|
581
|
+
}
|
|
582
|
+
|
|
583
|
+
return ["1", "true", "yes", "y"].includes(normalized);
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
async function chooseApiUrl(rl, currentUrl) {
|
|
587
|
+
const currentLabel = labelForApiUrl(currentUrl).toLowerCase();
|
|
588
|
+
const choice = await chooseFromList(
|
|
589
|
+
rl,
|
|
590
|
+
"Select your API URL",
|
|
591
|
+
["demo", "production", "custom"],
|
|
592
|
+
["demo", "production"].includes(currentLabel) ? currentLabel : "demo",
|
|
593
|
+
);
|
|
594
|
+
|
|
595
|
+
if (choice === "custom") {
|
|
596
|
+
return normalizeApiUrl(await question(rl, "Enter custom API URL: "), currentUrl);
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return normalizeApiUrl(choice, currentUrl);
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
function inferProductOptions(context) {
|
|
603
|
+
const localSecrets = readLocalSecrets(context.root);
|
|
604
|
+
const podfile = context.texts.podfile || "";
|
|
605
|
+
const androidAppBuildGradle = context.texts.androidAppBuildGradle || "";
|
|
606
|
+
const incodeFlow = context.texts.incodeFlow || "";
|
|
607
|
+
const incodeRuntimeConfig = context.texts.incodeRuntimeConfig || "";
|
|
608
|
+
const currentDependency = parseCurrentDependency(context.sdkDependency);
|
|
609
|
+
|
|
610
|
+
const currentIdv =
|
|
611
|
+
parseStringConstant(incodeFlow, "IDV_PRESET") ||
|
|
612
|
+
normalizeIdv(parseStringConstant(incodeFlow, "FLOW_PRESET"), "standard");
|
|
613
|
+
const sdkConfigMode = normalizeConfigMode(parseStringConstant(incodeFlow, "SDK_CONFIGURATION_MODE"), "manual");
|
|
614
|
+
const flowHandling = normalizeFlowHandling(parseStringConstant(incodeFlow, "FLOW_HANDLING"), "end-to-end");
|
|
615
|
+
const dashboardUrl = parseStringConstant(incodeFlow, "DASHBOARD_FLOW_URL");
|
|
616
|
+
const apiUrl = normalizeApiUrl(
|
|
617
|
+
localSecrets.INCODE_API_URL || parseStringConstant(incodeRuntimeConfig, "INCODE_DEFAULT_API_URL"),
|
|
618
|
+
API_URLS.demo.url,
|
|
619
|
+
);
|
|
620
|
+
const backendSessions = parseBooleanConstant(
|
|
621
|
+
incodeRuntimeConfig,
|
|
622
|
+
"INCODE_BACKEND_SESSIONS",
|
|
623
|
+
true,
|
|
624
|
+
);
|
|
625
|
+
const localBackendSessions = localSecrets.INCODE_BACKEND_SESSIONS
|
|
626
|
+
? normalizeBackendSessions(localSecrets.INCODE_BACKEND_SESSIONS, backendSessions)
|
|
627
|
+
: backendSessions;
|
|
628
|
+
const testMode = parseBooleanConstant(incodeRuntimeConfig, "INCODE_DEFAULT_TEST_MODE", false);
|
|
629
|
+
const idv = normalizeIdv(currentIdv, idvForVariant(currentDependency.variant));
|
|
630
|
+
const localNpmToken = localSecrets.NPM_TOKEN || localSecrets.NODE_AUTH_TOKEN || "";
|
|
631
|
+
|
|
632
|
+
return {
|
|
633
|
+
apiKey: localSecrets.INCODE_API_KEY || "",
|
|
634
|
+
apiKeyProvided: Boolean(localSecrets.INCODE_API_KEY),
|
|
635
|
+
apiUrl,
|
|
636
|
+
backendSessions: localBackendSessions,
|
|
637
|
+
configurationId: extractConfigurationId(dashboardUrl),
|
|
638
|
+
dashboardUrl,
|
|
639
|
+
dynamicLocalization: androidAppBuildGradle.includes("com.incode.sdk:extensions"),
|
|
640
|
+
flowHandling,
|
|
641
|
+
githubToken: localSecrets.GITHUB_TOKEN || "",
|
|
642
|
+
githubTokenProvided: Boolean(process.env.GITHUB_TOKEN || localSecrets.GITHUB_TOKEN),
|
|
643
|
+
githubUsername: localSecrets.GITHUB_USERNAME || "",
|
|
644
|
+
githubUsernameProvided: Boolean(process.env.GITHUB_USERNAME || localSecrets.GITHUB_USERNAME),
|
|
645
|
+
idv,
|
|
646
|
+
npmToken: localNpmToken,
|
|
647
|
+
npmTokenProvided: Boolean(process.env.NPM_TOKEN || process.env.NODE_AUTH_TOKEN || localNpmToken),
|
|
648
|
+
podSource: podfile.includes("https://github.com/Incode-Technologies-Example-Repos")
|
|
649
|
+
? "https"
|
|
650
|
+
: "ssh",
|
|
651
|
+
sdkConfigMode,
|
|
652
|
+
sdkVariant: currentDependency.variant,
|
|
653
|
+
sdkVersion: currentDependency.version,
|
|
654
|
+
testMode,
|
|
655
|
+
videoStreaming: androidAppBuildGradle.includes("com.incode.sdk:video-streaming"),
|
|
656
|
+
};
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
async function promptForProductOptions(context, options) {
|
|
660
|
+
const current = inferProductOptions(context);
|
|
661
|
+
const rl = readline.createInterface({
|
|
662
|
+
input: process.stdin,
|
|
663
|
+
output: process.stdout,
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
console.log("");
|
|
667
|
+
console.log("Incode React Native integration");
|
|
668
|
+
console.log("Platform is fixed to React Native for this hackathon agent.");
|
|
669
|
+
|
|
670
|
+
try {
|
|
671
|
+
const githubUsername = await question(
|
|
672
|
+
rl,
|
|
673
|
+
`GitHub username for Incode packages [${current.githubUsername || "blank"}]: `,
|
|
674
|
+
);
|
|
675
|
+
const githubToken = await secretQuestion(
|
|
676
|
+
rl,
|
|
677
|
+
"Paste your Incode customer GitHub token (blank to use env/skip): ",
|
|
678
|
+
);
|
|
679
|
+
const npmToken = await secretQuestion(
|
|
680
|
+
rl,
|
|
681
|
+
"Paste your React Native NPM token (blank to use env/skip): ",
|
|
682
|
+
);
|
|
683
|
+
const apiUrl = await chooseApiUrl(rl, current.apiUrl);
|
|
684
|
+
const apiKey = await secretQuestion(
|
|
685
|
+
rl,
|
|
686
|
+
"Paste your API key (blank if backend initiates sessions): ",
|
|
687
|
+
);
|
|
688
|
+
const backendSessions = await chooseBoolean(
|
|
689
|
+
rl,
|
|
690
|
+
"Do you initiate sessions on the backend? This enables the no-api-key client approach.",
|
|
691
|
+
current.backendSessions,
|
|
692
|
+
);
|
|
693
|
+
const sdkConfigMode = await chooseFromList(
|
|
694
|
+
rl,
|
|
695
|
+
"How do you want to configure the SDK?",
|
|
696
|
+
CONFIG_MODE_CHOICES,
|
|
697
|
+
current.sdkConfigMode,
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
let dashboardUrl = current.dashboardUrl;
|
|
701
|
+
let idv = current.idv;
|
|
702
|
+
|
|
703
|
+
if (sdkConfigMode === "online") {
|
|
704
|
+
console.log("");
|
|
705
|
+
console.log(
|
|
706
|
+
`Configure your flow at ${DASHBOARD_URL} in the ${labelForApiUrl(apiUrl)} environment, then paste the flow URL.`,
|
|
707
|
+
);
|
|
708
|
+
dashboardUrl = await question(rl, `Dashboard flow URL [${current.dashboardUrl || "blank"}]: `);
|
|
709
|
+
dashboardUrl = dashboardUrl.trim() || current.dashboardUrl;
|
|
710
|
+
idv =
|
|
711
|
+
inferIdvFromDashboardUrl(dashboardUrl) ||
|
|
712
|
+
current.idv ||
|
|
713
|
+
"standard";
|
|
714
|
+
} else {
|
|
715
|
+
idv = await chooseFromList(
|
|
716
|
+
rl,
|
|
717
|
+
"What kind of IDV do you want to perform?",
|
|
718
|
+
IDV_CHOICES,
|
|
719
|
+
current.idv,
|
|
720
|
+
);
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
const flowHandling = await chooseFromList(
|
|
724
|
+
rl,
|
|
725
|
+
"How would you like to handle the verification flow?",
|
|
726
|
+
FLOW_HANDLING_CHOICES,
|
|
727
|
+
current.flowHandling,
|
|
728
|
+
);
|
|
729
|
+
const versionChoices = [...VERSION_CHOICES, "custom"];
|
|
730
|
+
const defaultVersion = VERSION_CHOICES.includes(current.sdkVersion)
|
|
731
|
+
? current.sdkVersion
|
|
732
|
+
: DEFAULT_SDK_VERSION;
|
|
733
|
+
const versionChoice = await chooseFromList(
|
|
734
|
+
rl,
|
|
735
|
+
"SDK version",
|
|
736
|
+
versionChoices,
|
|
737
|
+
defaultVersion,
|
|
738
|
+
);
|
|
739
|
+
const sdkVersion =
|
|
740
|
+
versionChoice === "custom"
|
|
741
|
+
? normalizeVersion(await question(rl, "Enter SDK version, for example 9.14.0: "))
|
|
742
|
+
: versionChoice;
|
|
743
|
+
const podSource = await chooseFromList(
|
|
744
|
+
rl,
|
|
745
|
+
"iOS podspec source",
|
|
746
|
+
POD_SOURCE_CHOICES,
|
|
747
|
+
current.podSource,
|
|
748
|
+
);
|
|
749
|
+
return {
|
|
750
|
+
...current,
|
|
751
|
+
apiKey: apiKey.trim() || current.apiKey,
|
|
752
|
+
apiKeyProvided: Boolean(apiKey.trim() || current.apiKey),
|
|
753
|
+
apiUrl,
|
|
754
|
+
backendSessions,
|
|
755
|
+
configurationId: extractConfigurationId(dashboardUrl),
|
|
756
|
+
dashboardUrl,
|
|
757
|
+
flowHandling,
|
|
758
|
+
githubToken: githubToken.trim() || current.githubToken,
|
|
759
|
+
githubTokenProvided: Boolean(githubToken.trim()) || current.githubTokenProvided,
|
|
760
|
+
githubUsername: githubUsername.trim() || current.githubUsername,
|
|
761
|
+
githubUsernameProvided: Boolean(githubUsername.trim()) || current.githubUsernameProvided,
|
|
762
|
+
idv,
|
|
763
|
+
npmToken: npmToken.trim() || current.npmToken,
|
|
764
|
+
npmTokenProvided: Boolean(npmToken.trim()) || current.npmTokenProvided,
|
|
765
|
+
podSource,
|
|
766
|
+
sdkConfigMode,
|
|
767
|
+
sdkVariant: variantForIdv(idv),
|
|
768
|
+
sdkVersion,
|
|
769
|
+
videoStreaming: Boolean(IDV_PRESETS[idv].videoStreaming),
|
|
770
|
+
};
|
|
771
|
+
} finally {
|
|
772
|
+
rl.close();
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
|
|
776
|
+
function chooseProductOptions(context, options) {
|
|
777
|
+
const current = inferProductOptions(context);
|
|
778
|
+
const hasExplicitOption =
|
|
779
|
+
options.apiKey ||
|
|
780
|
+
options.apiUrl ||
|
|
781
|
+
options.backendSessions ||
|
|
782
|
+
options.configurationId ||
|
|
783
|
+
options.dashboardUrl ||
|
|
784
|
+
options.dynamicLocalization !== undefined ||
|
|
785
|
+
options.flow ||
|
|
786
|
+
options.flowHandling ||
|
|
787
|
+
options.githubToken ||
|
|
788
|
+
options.githubUsername ||
|
|
789
|
+
options.idv ||
|
|
790
|
+
options.npmToken ||
|
|
791
|
+
options.podSource ||
|
|
792
|
+
options.sdkConfig ||
|
|
793
|
+
options.testMode !== undefined ||
|
|
794
|
+
options.version ||
|
|
795
|
+
options.videoStreaming !== undefined;
|
|
796
|
+
|
|
797
|
+
const shouldPrompt = process.stdin.isTTY && process.stdout.isTTY && !options.defaults && !hasExplicitOption;
|
|
798
|
+
if (shouldPrompt) {
|
|
799
|
+
return promptForProductOptions(context, options);
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
const sdkConfigMode = normalizeConfigMode(options.sdkConfig, current.sdkConfigMode);
|
|
803
|
+
const dashboardUrl = options.dashboardUrl || current.dashboardUrl;
|
|
804
|
+
const dashboardIdv = inferIdvFromDashboardUrl(dashboardUrl);
|
|
805
|
+
const idv =
|
|
806
|
+
sdkConfigMode === "online" && dashboardIdv
|
|
807
|
+
? dashboardIdv
|
|
808
|
+
: normalizeIdv(options.idv || options.flow, current.idv);
|
|
809
|
+
const preset = IDV_PRESETS[idv] || IDV_PRESETS.standard;
|
|
810
|
+
|
|
811
|
+
return {
|
|
812
|
+
...current,
|
|
813
|
+
apiKey: options.apiKey || current.apiKey,
|
|
814
|
+
apiKeyProvided: Boolean(options.apiKey || current.apiKey),
|
|
815
|
+
apiUrl: normalizeApiUrl(options.apiUrl, current.apiUrl),
|
|
816
|
+
backendSessions: normalizeBackendSessions(options.backendSessions, current.backendSessions),
|
|
817
|
+
configurationId: options.configurationId || extractConfigurationId(dashboardUrl),
|
|
818
|
+
dashboardUrl,
|
|
819
|
+
dynamicLocalization:
|
|
820
|
+
options.dynamicLocalization === undefined
|
|
821
|
+
? current.dynamicLocalization
|
|
822
|
+
: options.dynamicLocalization,
|
|
823
|
+
flowHandling: normalizeFlowHandling(options.flowHandling, current.flowHandling),
|
|
824
|
+
githubToken: options.githubToken || current.githubToken,
|
|
825
|
+
githubTokenProvided: Boolean(options.githubToken || process.env.GITHUB_TOKEN || current.githubToken),
|
|
826
|
+
githubUsername: options.githubUsername || current.githubUsername,
|
|
827
|
+
githubUsernameProvided: Boolean(
|
|
828
|
+
options.githubUsername || process.env.GITHUB_USERNAME || current.githubUsername,
|
|
829
|
+
),
|
|
830
|
+
idv,
|
|
831
|
+
npmToken: options.npmToken || current.npmToken,
|
|
832
|
+
npmTokenProvided: Boolean(
|
|
833
|
+
options.npmToken || process.env.NPM_TOKEN || process.env.NODE_AUTH_TOKEN || current.npmToken,
|
|
834
|
+
),
|
|
835
|
+
podSource: normalizeChoice(options.podSource, POD_SOURCE_CHOICES, current.podSource),
|
|
836
|
+
sdkConfigMode,
|
|
837
|
+
sdkVariant: variantForIdv(idv),
|
|
838
|
+
sdkVersion: normalizeVersion(options.version || DEFAULT_SDK_VERSION),
|
|
839
|
+
testMode: options.testMode === undefined ? current.testMode : options.testMode,
|
|
840
|
+
videoStreaming:
|
|
841
|
+
options.videoStreaming === undefined
|
|
842
|
+
? Boolean(preset.videoStreaming)
|
|
843
|
+
: options.videoStreaming,
|
|
844
|
+
};
|
|
845
|
+
}
|
|
846
|
+
|
|
847
|
+
function chooseSdkSelection(productOptions) {
|
|
848
|
+
return {
|
|
849
|
+
dependency: dependencyForRegistry(productOptions.sdkVersion, productOptions.sdkVariant),
|
|
850
|
+
source: "npm registry",
|
|
851
|
+
variant: productOptions.sdkVariant,
|
|
852
|
+
version: productOptions.sdkVersion,
|
|
853
|
+
};
|
|
854
|
+
}
|
|
855
|
+
|
|
856
|
+
function toTs(value) {
|
|
857
|
+
return JSON.stringify(value, null, 2);
|
|
858
|
+
}
|
|
859
|
+
|
|
860
|
+
function createFlowConfigFile(productOptions) {
|
|
861
|
+
const flowPreset = IDV_PRESETS[productOptions.idv] || IDV_PRESETS.standard;
|
|
862
|
+
const listeners = flowPreset.listeners
|
|
863
|
+
.map(
|
|
864
|
+
([module, label]) =>
|
|
865
|
+
` { label: '${label}', module: '${module}' },`,
|
|
866
|
+
)
|
|
867
|
+
.join("\n");
|
|
868
|
+
const sessionConfig = productOptions.configurationId
|
|
869
|
+
? { configurationId: productOptions.configurationId }
|
|
870
|
+
: {};
|
|
871
|
+
|
|
872
|
+
return `import type {
|
|
873
|
+
OnboardingFlowConfig,
|
|
874
|
+
OnboardingRecordSessionConfig,
|
|
875
|
+
OnboardingSessionConfig,
|
|
876
|
+
StepCompleteListener,
|
|
877
|
+
} from '@incode-sdks/react-native-incode-sdk';
|
|
878
|
+
|
|
879
|
+
export type SdkConfigurationMode = 'online' | 'manual';
|
|
880
|
+
|
|
881
|
+
export type FlowHandling = 'end-to-end' | 'step-by-step';
|
|
882
|
+
|
|
883
|
+
export const SDK_CONFIGURATION_MODE: SdkConfigurationMode = '${productOptions.sdkConfigMode}';
|
|
884
|
+
|
|
885
|
+
export const DASHBOARD_FLOW_URL = '${productOptions.dashboardUrl || ""}';
|
|
886
|
+
|
|
887
|
+
export const IDV_PRESET = '${productOptions.idv}' as const;
|
|
888
|
+
|
|
889
|
+
export const FLOW_HANDLING: FlowHandling = '${productOptions.flowHandling}';
|
|
890
|
+
|
|
891
|
+
export const FLOW_LABEL = '${flowPreset.label}';
|
|
892
|
+
|
|
893
|
+
export const FLOW_DESCRIPTION = '${flowPreset.description}';
|
|
894
|
+
|
|
895
|
+
export const SESSION_CONFIG: OnboardingSessionConfig = ${toTs(sessionConfig)};
|
|
896
|
+
|
|
897
|
+
export const FLOW_RECORD_SESSION_CONFIG: OnboardingRecordSessionConfig | undefined = ${
|
|
898
|
+
flowPreset.recordSessionConfig ? toTs(flowPreset.recordSessionConfig) : "undefined"
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
export const FLOW_CONFIG: OnboardingFlowConfig = ${toTs(flowPreset.modules)};
|
|
902
|
+
|
|
903
|
+
export const SECTION_FLOW_CONFIGS: Array<{
|
|
904
|
+
flowConfig: OnboardingFlowConfig;
|
|
905
|
+
sectionTag: string;
|
|
906
|
+
}> = ${toTs(flowPreset.sections)};
|
|
907
|
+
|
|
908
|
+
export const STEP_LISTENERS: Array<{
|
|
909
|
+
label: string;
|
|
910
|
+
module: StepCompleteListener['module'];
|
|
911
|
+
}> = [
|
|
912
|
+
${listeners}
|
|
913
|
+
];
|
|
914
|
+
`;
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function createRuntimeConfigFile(productOptions) {
|
|
918
|
+
return `export const INCODE_DEFAULT_API_URL = '${productOptions.apiUrl}';
|
|
919
|
+
|
|
920
|
+
export const INCODE_API_URL_LABEL = '${labelForApiUrl(productOptions.apiUrl)}';
|
|
921
|
+
|
|
922
|
+
export const INCODE_BACKEND_SESSIONS = ${productOptions.backendSessions};
|
|
923
|
+
|
|
924
|
+
export const INCODE_DEFAULT_TEST_MODE = ${productOptions.testMode};
|
|
925
|
+
`;
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
function createLocalCredentialEnvFile(productOptions) {
|
|
929
|
+
const lines = [
|
|
930
|
+
"# Local Incode SDK credentials and runtime values. This file is ignored by git.",
|
|
931
|
+
"# Load it with: source .env.local",
|
|
932
|
+
`export INCODE_API_URL="${escapeEnvValue(productOptions.apiUrl)}"`,
|
|
933
|
+
`export INCODE_BACKEND_SESSIONS="${productOptions.backendSessions ? "true" : "false"}"`,
|
|
934
|
+
];
|
|
935
|
+
|
|
936
|
+
if (productOptions.githubToken) {
|
|
937
|
+
lines.push(
|
|
938
|
+
`export GITHUB_USERNAME="${escapeEnvValue(
|
|
939
|
+
productOptions.githubUsername || "<your-github-username>",
|
|
940
|
+
)}"`,
|
|
941
|
+
);
|
|
942
|
+
lines.push(`export GITHUB_TOKEN="${escapeEnvValue(productOptions.githubToken)}"`);
|
|
943
|
+
} else if (productOptions.githubUsername) {
|
|
944
|
+
lines.push(`export GITHUB_USERNAME="${escapeEnvValue(productOptions.githubUsername)}"`);
|
|
945
|
+
lines.push('export GITHUB_TOKEN="${GITHUB_TOKEN:-<your-github-token>}"');
|
|
946
|
+
}
|
|
947
|
+
|
|
948
|
+
if (productOptions.apiKey) {
|
|
949
|
+
lines.push(`export INCODE_API_KEY="${escapeEnvValue(productOptions.apiKey)}"`);
|
|
950
|
+
}
|
|
951
|
+
|
|
952
|
+
if (productOptions.backendSessions) {
|
|
953
|
+
lines.push('export INCODE_SESSION_TOKEN="${INCODE_SESSION_TOKEN:-<token-from-your-backend>}"');
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return `${lines.join("\n")}\n`;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
function createNpmrcFile(productOptions) {
|
|
960
|
+
return `# Local React Native SDK NPM credential. This file is ignored by git.
|
|
961
|
+
@incode-sdks:registry=https://registry.npmjs.org/
|
|
962
|
+
//registry.npmjs.org/:_authToken=${productOptions.npmToken}
|
|
963
|
+
`;
|
|
964
|
+
}
|
|
965
|
+
|
|
966
|
+
function createExampleFile() {
|
|
967
|
+
return `import IncodeSdk from '@incode-sdks/react-native-incode-sdk';
|
|
968
|
+
import {
|
|
969
|
+
FLOW_CONFIG,
|
|
970
|
+
FLOW_HANDLING,
|
|
971
|
+
FLOW_RECORD_SESSION_CONFIG,
|
|
972
|
+
SECTION_FLOW_CONFIGS,
|
|
973
|
+
SESSION_CONFIG,
|
|
974
|
+
} from './incodeFlow';
|
|
975
|
+
|
|
976
|
+
export type StartIncodeOptions = {
|
|
977
|
+
apiKey?: string;
|
|
978
|
+
apiUrl: string;
|
|
979
|
+
configurationId?: string;
|
|
980
|
+
interviewId?: string;
|
|
981
|
+
// Backend sessions should pass the backend-created session_token here.
|
|
982
|
+
// The React Native SDK expects it as sessionConfig.token.
|
|
983
|
+
sessionToken?: string;
|
|
984
|
+
testMode?: boolean;
|
|
985
|
+
};
|
|
986
|
+
|
|
987
|
+
function backendSessionApiUrl(apiUrl: string) {
|
|
988
|
+
return \`\${apiUrl.trim().replace(/\\/+$/, '')}/0/\`;
|
|
989
|
+
}
|
|
990
|
+
|
|
991
|
+
function buildSessionConfig(options: StartIncodeOptions) {
|
|
992
|
+
return {
|
|
993
|
+
...SESSION_CONFIG,
|
|
994
|
+
...(options.configurationId ? { configurationId: options.configurationId } : {}),
|
|
995
|
+
...(options.interviewId ? { interviewId: options.interviewId } : {}),
|
|
996
|
+
...(options.sessionToken ? { token: options.sessionToken } : {}),
|
|
997
|
+
};
|
|
998
|
+
}
|
|
999
|
+
|
|
1000
|
+
function recordSessionConfig() {
|
|
1001
|
+
return FLOW_RECORD_SESSION_CONFIG
|
|
1002
|
+
? { recordSessionConfig: FLOW_RECORD_SESSION_CONFIG }
|
|
1003
|
+
: {};
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
export async function startConfiguredOnboarding(options: StartIncodeOptions) {
|
|
1007
|
+
const usesBackendSession = Boolean(options.sessionToken);
|
|
1008
|
+
const apiConfig: { key?: string; url: string } = {
|
|
1009
|
+
url: usesBackendSession ? backendSessionApiUrl(options.apiUrl) : options.apiUrl,
|
|
1010
|
+
};
|
|
1011
|
+
|
|
1012
|
+
if (options.apiKey) {
|
|
1013
|
+
apiConfig.key = options.apiKey;
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
await IncodeSdk.initialize({
|
|
1017
|
+
testMode: options.testMode ?? false,
|
|
1018
|
+
apiConfig,
|
|
1019
|
+
});
|
|
1020
|
+
|
|
1021
|
+
const sessionConfig = buildSessionConfig(options);
|
|
1022
|
+
|
|
1023
|
+
if (FLOW_HANDLING === 'step-by-step') {
|
|
1024
|
+
const session = await IncodeSdk.setupOnboardingSession({ sessionConfig });
|
|
1025
|
+
let lastResult: Awaited<ReturnType<typeof IncodeSdk.startOnboardingSection>> | undefined;
|
|
1026
|
+
|
|
1027
|
+
for (const section of SECTION_FLOW_CONFIGS) {
|
|
1028
|
+
lastResult = await IncodeSdk.startOnboardingSection({
|
|
1029
|
+
flowConfig: section.flowConfig,
|
|
1030
|
+
sectionTag: section.sectionTag,
|
|
1031
|
+
...recordSessionConfig(),
|
|
1032
|
+
});
|
|
1033
|
+
|
|
1034
|
+
if (lastResult.status === 'userCancelled') {
|
|
1035
|
+
return {
|
|
1036
|
+
...lastResult,
|
|
1037
|
+
interviewId: session.interviewId,
|
|
1038
|
+
token: session.token,
|
|
1039
|
+
};
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
await IncodeSdk.finishOnboardingFlow();
|
|
1044
|
+
|
|
1045
|
+
return {
|
|
1046
|
+
status: lastResult?.status || 'success',
|
|
1047
|
+
interviewId: session.interviewId,
|
|
1048
|
+
sectionTag: lastResult?.sectionTag,
|
|
1049
|
+
token: session.token,
|
|
1050
|
+
};
|
|
1051
|
+
}
|
|
1052
|
+
|
|
1053
|
+
return IncodeSdk.startOnboarding({
|
|
1054
|
+
flowConfig: FLOW_CONFIG,
|
|
1055
|
+
sessionConfig,
|
|
1056
|
+
...recordSessionConfig(),
|
|
1057
|
+
});
|
|
1058
|
+
}
|
|
1059
|
+
|
|
1060
|
+
export async function startIncodeOnboarding(apiUrl: string, apiKey?: string) {
|
|
1061
|
+
return startConfiguredOnboarding({
|
|
1062
|
+
apiKey,
|
|
1063
|
+
apiUrl,
|
|
1064
|
+
testMode: false,
|
|
1065
|
+
});
|
|
1066
|
+
}
|
|
1067
|
+
`;
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function androidFeatureDependencies(productOptions) {
|
|
1071
|
+
const dependencies = [];
|
|
1072
|
+
|
|
1073
|
+
if (productOptions.videoStreaming) {
|
|
1074
|
+
dependencies.push({
|
|
1075
|
+
notation: "com.incode.sdk:video-streaming:1.5.5",
|
|
1076
|
+
});
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
if (productOptions.dynamicLocalization) {
|
|
1080
|
+
dependencies.push({
|
|
1081
|
+
notation: "com.incode.sdk:extensions:1.2.1",
|
|
1082
|
+
});
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
return dependencies;
|
|
1086
|
+
}
|
|
1087
|
+
|
|
1088
|
+
function generatePlan(context, sdkSelection, productOptions) {
|
|
1089
|
+
const sdkDependency = sdkSelection.dependency;
|
|
1090
|
+
const featureDependencies = androidFeatureDependencies(productOptions);
|
|
1091
|
+
const changes = [
|
|
1092
|
+
{
|
|
1093
|
+
action: "gitignore_entries",
|
|
1094
|
+
entries: [".env.local", ".incode.local.env", ".npmrc"],
|
|
1095
|
+
file: ".gitignore",
|
|
1096
|
+
title: "Keep local SDK credentials out of source control",
|
|
1097
|
+
},
|
|
1098
|
+
{
|
|
1099
|
+
action: "json_dependency",
|
|
1100
|
+
file: context.files.packageJson,
|
|
1101
|
+
name: SDK_PACKAGE,
|
|
1102
|
+
title: `Install ${SDK_PACKAGE}`,
|
|
1103
|
+
version: sdkDependency,
|
|
1104
|
+
},
|
|
1105
|
+
{
|
|
1106
|
+
action: "pod_sources",
|
|
1107
|
+
file: context.files.podfile,
|
|
1108
|
+
sources: [
|
|
1109
|
+
"source 'https://cdn.cocoapods.org/'",
|
|
1110
|
+
podSourceForMode(productOptions.podSource),
|
|
1111
|
+
],
|
|
1112
|
+
title: "Add Incode iOS podspec sources",
|
|
1113
|
+
},
|
|
1114
|
+
{
|
|
1115
|
+
action: "android_repositories",
|
|
1116
|
+
credentialsMode: "environment",
|
|
1117
|
+
file: context.files.androidBuildGradle,
|
|
1118
|
+
title: "Add Android Maven repositories and credential lookup",
|
|
1119
|
+
},
|
|
1120
|
+
{
|
|
1121
|
+
action: "android_multidex",
|
|
1122
|
+
file: context.files.androidAppBuildGradle,
|
|
1123
|
+
title: "Enable Android multidex",
|
|
1124
|
+
},
|
|
1125
|
+
{
|
|
1126
|
+
action: "android_permissions",
|
|
1127
|
+
file: context.files.androidManifest,
|
|
1128
|
+
permissions: [
|
|
1129
|
+
"android.permission.CAMERA",
|
|
1130
|
+
"android.permission.ACCESS_FINE_LOCATION",
|
|
1131
|
+
"android.permission.ACCESS_COARSE_LOCATION",
|
|
1132
|
+
"android.permission.RECORD_AUDIO",
|
|
1133
|
+
],
|
|
1134
|
+
title: "Add Android runtime permissions",
|
|
1135
|
+
},
|
|
1136
|
+
{
|
|
1137
|
+
action: "write_file",
|
|
1138
|
+
content: createFlowConfigFile(productOptions),
|
|
1139
|
+
file: "src/incodeFlow.ts",
|
|
1140
|
+
title: `Generate ${productOptions.idv} ${productOptions.flowHandling} flow`,
|
|
1141
|
+
},
|
|
1142
|
+
{
|
|
1143
|
+
action: "write_file",
|
|
1144
|
+
content: createRuntimeConfigFile(productOptions),
|
|
1145
|
+
file: "src/incodeRuntimeConfig.ts",
|
|
1146
|
+
title: "Generate non-secret runtime defaults",
|
|
1147
|
+
},
|
|
1148
|
+
{
|
|
1149
|
+
action: "write_file",
|
|
1150
|
+
content: createExampleFile(),
|
|
1151
|
+
file: "src/IncodeExample.ts",
|
|
1152
|
+
title: "Create end-to-end and step-by-step onboarding example",
|
|
1153
|
+
},
|
|
1154
|
+
];
|
|
1155
|
+
|
|
1156
|
+
if (featureDependencies.length > 0) {
|
|
1157
|
+
changes.push({
|
|
1158
|
+
action: "android_dependencies",
|
|
1159
|
+
dependencies: featureDependencies,
|
|
1160
|
+
file: context.files.androidAppBuildGradle,
|
|
1161
|
+
title: "Add selected Incode Android feature dependencies",
|
|
1162
|
+
});
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
if (
|
|
1166
|
+
productOptions.githubToken ||
|
|
1167
|
+
productOptions.githubUsername ||
|
|
1168
|
+
productOptions.apiKey ||
|
|
1169
|
+
productOptions.backendSessions
|
|
1170
|
+
) {
|
|
1171
|
+
changes.push({
|
|
1172
|
+
action: "write_file",
|
|
1173
|
+
content: createLocalCredentialEnvFile(productOptions),
|
|
1174
|
+
file: ".env.local",
|
|
1175
|
+
title: "Write ignored local environment values",
|
|
1176
|
+
});
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
if (productOptions.npmToken) {
|
|
1180
|
+
changes.push({
|
|
1181
|
+
action: "write_file",
|
|
1182
|
+
content: createNpmrcFile(productOptions),
|
|
1183
|
+
file: ".npmrc",
|
|
1184
|
+
title: "Write ignored local NPM token",
|
|
1185
|
+
});
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
if (context.files.infoPlist) {
|
|
1189
|
+
changes.push({
|
|
1190
|
+
action: "plist_strings",
|
|
1191
|
+
entries: {
|
|
1192
|
+
NSCameraUsageDescription:
|
|
1193
|
+
"The Incode SDK uses the camera to verify identity documents and capture selfies.",
|
|
1194
|
+
NSLocationWhenInUseUsageDescription:
|
|
1195
|
+
"The Incode SDK can use location during geolocation verification steps.",
|
|
1196
|
+
NSMicrophoneUsageDescription:
|
|
1197
|
+
"The Incode SDK uses the microphone for video conference or video selfie flows.",
|
|
1198
|
+
},
|
|
1199
|
+
file: context.files.infoPlist,
|
|
1200
|
+
title: "Add iOS privacy usage descriptions",
|
|
1201
|
+
});
|
|
1202
|
+
}
|
|
1203
|
+
|
|
1204
|
+
if (context.files.mainApplication) {
|
|
1205
|
+
changes.push({
|
|
1206
|
+
action: "main_application_multidex",
|
|
1207
|
+
file: context.files.mainApplication,
|
|
1208
|
+
title: "Use MultiDexApplication",
|
|
1209
|
+
});
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
return {
|
|
1213
|
+
changes,
|
|
1214
|
+
productOptions,
|
|
1215
|
+
root: context.root,
|
|
1216
|
+
sdkDependency,
|
|
1217
|
+
sdkSelection,
|
|
1218
|
+
};
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
function yesNo(value) {
|
|
1222
|
+
return value ? "yes" : "no";
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
function printPlan(context, plan, options) {
|
|
1226
|
+
const product = plan.productOptions;
|
|
1227
|
+
const featureDeps = [
|
|
1228
|
+
product.videoStreaming ? "video-streaming" : "",
|
|
1229
|
+
product.dynamicLocalization ? "dynamic-localization" : "",
|
|
1230
|
+
].filter(Boolean);
|
|
1231
|
+
|
|
1232
|
+
console.log(`Project: ${context.root}`);
|
|
1233
|
+
console.log(`Platform: React Native`);
|
|
1234
|
+
console.log(`React Native: ${context.reactNativeVersion || "unknown"}`);
|
|
1235
|
+
console.log(`SDK source: ${plan.sdkSelection.source}`);
|
|
1236
|
+
console.log(`SDK version: ${plan.sdkSelection.version}`);
|
|
1237
|
+
console.log(`SDK variant: ${plan.sdkSelection.variant}`);
|
|
1238
|
+
console.log(`SDK dependency: ${plan.sdkDependency}`);
|
|
1239
|
+
console.log(`GitHub username: ${product.githubUsernameProvided ? "provided or in env" : "not provided"}`);
|
|
1240
|
+
console.log(`GitHub token: ${product.githubTokenProvided ? "provided or in env" : "not provided"}`);
|
|
1241
|
+
console.log(`NPM token: ${product.npmTokenProvided ? "provided or in env" : "not provided"}`);
|
|
1242
|
+
console.log(`API URL: ${labelForApiUrl(product.apiUrl)} (${product.apiUrl})`);
|
|
1243
|
+
console.log(`API key: ${product.apiKeyProvided ? "provided at prompt/CLI" : "not stored in source"}`);
|
|
1244
|
+
console.log(`Backend sessions: ${yesNo(product.backendSessions)}${product.backendSessions ? " (no-api-key client)" : " (client API key mode)"}`);
|
|
1245
|
+
if (product.backendSessions) {
|
|
1246
|
+
console.log("Backend session endpoint: src/IncodeExample.ts appends /0/ to apiConfig.url.");
|
|
1247
|
+
console.log("Backend session token: replace the demo UI input with your backend fetch, then pass session_token as sessionToken.");
|
|
1248
|
+
}
|
|
1249
|
+
console.log(`SDK configuration: ${product.sdkConfigMode}`);
|
|
1250
|
+
if (product.sdkConfigMode === "online") {
|
|
1251
|
+
console.log(`Dashboard flow URL: ${product.dashboardUrl || "not provided"}`);
|
|
1252
|
+
console.log(`Dashboard environment: ${labelForApiUrl(product.apiUrl)}`);
|
|
1253
|
+
console.log(`Configuration ID: ${product.configurationId || "not inferred"}`);
|
|
1254
|
+
console.log("Dashboard flow config: will be fetched by API when the flow-config endpoint is connected.");
|
|
1255
|
+
}
|
|
1256
|
+
console.log(`IDV preset: ${product.idv}`);
|
|
1257
|
+
console.log(`Flow handling: ${product.flowHandling}`);
|
|
1258
|
+
console.log(`Default testMode: ${product.testMode}`);
|
|
1259
|
+
console.log(`iOS podspec source: ${product.podSource}`);
|
|
1260
|
+
console.log("Android credentials: GITHUB_USERNAME and GITHUB_TOKEN environment variables");
|
|
1261
|
+
console.log(`Feature deps: ${featureDeps.join(", ") || "none"}`);
|
|
1262
|
+
console.log("");
|
|
1263
|
+
console.log(
|
|
1264
|
+
"Secrets are never written to generated TypeScript, Gradle, Podfile, or README files.",
|
|
1265
|
+
);
|
|
1266
|
+
console.log(
|
|
1267
|
+
"When --apply receives tokens, they are written only to ignored local files: .env.local and .npmrc.",
|
|
1268
|
+
);
|
|
1269
|
+
console.log("");
|
|
1270
|
+
console.log(options.apply ? "Applying changes:" : "Planned changes:");
|
|
1271
|
+
|
|
1272
|
+
for (const change of plan.changes) {
|
|
1273
|
+
console.log(`- ${change.title} (${change.file})`);
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
if (!options.apply) {
|
|
1277
|
+
console.log("");
|
|
1278
|
+
console.log("Dry run only. Re-run with `--apply` to update the project.");
|
|
1279
|
+
console.log(
|
|
1280
|
+
"Example: npx @incode-sdks/incode-integrate --version 9.14.0 --pod-source https --api-url demo --backend-sessions yes --sdk-config manual --idv standard-nfc --flow-handling step-by-step --apply",
|
|
1281
|
+
);
|
|
1282
|
+
}
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
async function run() {
|
|
1286
|
+
const options = parseArgs(process.argv.slice(2));
|
|
1287
|
+
const context = scanProject();
|
|
1288
|
+
const productOptions = await chooseProductOptions(context, options);
|
|
1289
|
+
const sdkSelection = chooseSdkSelection(productOptions);
|
|
1290
|
+
const plan = generatePlan(context, sdkSelection, productOptions);
|
|
1291
|
+
|
|
1292
|
+
printPlan(context, plan, options);
|
|
1293
|
+
|
|
1294
|
+
if (!options.apply) {
|
|
1295
|
+
return;
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
const results = applyPlan(plan);
|
|
1299
|
+
console.log("");
|
|
1300
|
+
|
|
1301
|
+
for (const result of results) {
|
|
1302
|
+
console.log(`${result.changed ? "updated" : "unchanged"} ${result.file}`);
|
|
1303
|
+
}
|
|
1304
|
+
|
|
1305
|
+
if (results.some(result => result.file === ".env.local")) {
|
|
1306
|
+
console.log("");
|
|
1307
|
+
console.log("Before Android builds, load local credentials with: source .env.local");
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
console.log("");
|
|
1311
|
+
console.log("Done. Run npm install, then cd ios && pod install when credentials are available.");
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
run().catch(error => {
|
|
1315
|
+
console.error(error.message);
|
|
1316
|
+
process.exitCode = 1;
|
|
1317
|
+
});
|