@jskit-ai/kernel 0.1.62 → 0.1.64

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.
@@ -506,6 +506,77 @@ test("registerHttpRuntime passes app context so request scope is available", asy
506
506
  assert.equal(fastify.setErrorHandlerCalls, 1);
507
507
  });
508
508
 
509
+ test("createRouter preserves explicit internal route flags", () => {
510
+ const router = createRouter();
511
+
512
+ router.get(
513
+ "/internal-only",
514
+ {
515
+ internal: true
516
+ },
517
+ async (_request, reply) => {
518
+ reply.code(204).send();
519
+ }
520
+ );
521
+
522
+ const [route] = router.list();
523
+ assert.equal(route?.internal, true);
524
+ });
525
+
526
+ test("registerHttpRuntime skips internal routes from public HTTP registration", () => {
527
+ const app = createApplication();
528
+ const fastify = createFastifyStub();
529
+ const router = createRouter();
530
+
531
+ router.get(
532
+ "/internal-only",
533
+ {
534
+ internal: true
535
+ },
536
+ async (_request, reply) => {
537
+ reply.code(200).send({ hidden: true });
538
+ }
539
+ );
540
+ router.get("/public-route", async (_request, reply) => {
541
+ reply.code(200).send({ ok: true });
542
+ });
543
+
544
+ app.instance("jskit.fastify", fastify);
545
+ app.instance("jskit.http.router", router);
546
+
547
+ const registration = registerHttpRuntime(app);
548
+ assert.equal(registration.routeCount, 1);
549
+ assert.equal(fastify.routes.length, 1);
550
+ assert.equal(fastify.routes[0].url, "/public-route");
551
+ });
552
+
553
+ test("registerHttpRuntime skips internal routes generated through router apiResource helpers", () => {
554
+ const app = createApplication();
555
+ const fastify = createFastifyStub();
556
+ const router = createRouter();
557
+
558
+ router.apiResource(
559
+ "widgets",
560
+ {
561
+ index: async (_request, reply) => reply.code(200).send({ ok: true }),
562
+ store: async (_request, reply) => reply.code(201).send({ ok: true }),
563
+ show: async (_request, reply) => reply.code(200).send({ ok: true }),
564
+ update: async (_request, reply) => reply.code(200).send({ ok: true }),
565
+ destroy: async (_request, reply) => reply.code(204).send()
566
+ },
567
+ {
568
+ internal: true
569
+ }
570
+ );
571
+
572
+ app.instance("jskit.fastify", fastify);
573
+ app.instance("jskit.http.router", router);
574
+
575
+ const registration = registerHttpRuntime(app);
576
+ assert.equal(registration.routeCount, 0);
577
+ assert.equal(fastify.routes.length, 0);
578
+ });
579
+
509
580
  test("registerHttpRuntime installs API error handling once by default", () => {
510
581
  const app = createApplication();
511
582
  const fastify = createFastifyStub();
@@ -310,15 +310,16 @@ function registerRoutes(
310
310
  }
311
311
 
312
312
  const normalizedRoutes = normalizeArray(routes);
313
+ const publicRoutes = normalizedRoutes.filter((route) => route?.internal !== true);
313
314
  const policyApplier = typeof applyRoutePolicy === "function" ? applyRoutePolicy : defaultApplyRoutePolicy;
314
315
  const fallbackHandler = typeof missingHandler === "function" ? missingHandler : defaultMissingHandler;
315
316
  const runtimeMiddlewareConfig = normalizeRuntimeMiddlewareConfig(middleware);
316
317
 
317
- if (normalizedRoutes.some((route) => routeRequiresJsonApiContentTypeParser(route))) {
318
+ if (publicRoutes.some((route) => routeRequiresJsonApiContentTypeParser(route))) {
318
319
  registerJsonApiContentTypeParser(fastify);
319
320
  }
320
321
 
321
- for (const route of normalizedRoutes) {
322
+ for (const route of publicRoutes) {
322
323
  const routeTransport = normalizeRouteTransport(route?.transport, {
323
324
  context: `Route ${String(route?.method || "<unknown>")} ${String(route?.path || "<unknown>")} transport`,
324
325
  ErrorType: RouteRegistrationError
@@ -407,7 +408,7 @@ function registerRoutes(
407
408
  }
408
409
 
409
410
  return {
410
- routeCount: normalizedRoutes.length
411
+ routeCount: publicRoutes.length
411
412
  };
412
413
  }
413
414
 
@@ -35,6 +35,18 @@ function normalizeRouterMiddlewareStack(value, { context = "middleware" } = {})
35
35
  });
36
36
  }
37
37
 
38
+ function normalizeRouteInternal(value, { method = "", path = "" } = {}) {
39
+ if (value == null) {
40
+ return false;
41
+ }
42
+ if (typeof value !== "boolean") {
43
+ throw new RouteDefinitionError(
44
+ `Route ${String(method || "<unknown>")} ${String(path || "<unknown>")} internal must be a boolean.`
45
+ );
46
+ }
47
+ return value;
48
+ }
49
+
38
50
  function normalizeRouteInput(method, path, optionsOrHandler, maybeHandler) {
39
51
  const options =
40
52
  typeof optionsOrHandler === "function"
@@ -108,6 +120,10 @@ class HttpRouter {
108
120
  auth: resolvedOptions.auth,
109
121
  contextPolicy: resolvedOptions.contextPolicy,
110
122
  surface: resolvedOptions.surface,
123
+ internal: normalizeRouteInternal(resolvedOptions.internal, {
124
+ method: input.method,
125
+ path: input.path
126
+ }),
111
127
  visibility: resolvedOptions.visibility,
112
128
  permission: resolvedOptions.permission,
113
129
  ownerParam: resolvedOptions.ownerParam,
@@ -186,9 +202,11 @@ class HttpRouter {
186
202
  const itemPath = `/${resourceName}/:${idParam}`;
187
203
 
188
204
  const methods = normalizeObject(controller);
189
- const middleware = normalizeRouterMiddlewareStack(options.middleware, {
190
- context: `resource ${resourceName} middleware`
191
- });
205
+ const routeOptions = {
206
+ ...normalizeObject(options)
207
+ };
208
+ delete routeOptions.idParam;
209
+ delete routeOptions.apiOnly;
192
210
 
193
211
  const requireMethod = (methodName) => {
194
212
  const handler = methods[methodName];
@@ -198,15 +216,15 @@ class HttpRouter {
198
216
  return handler;
199
217
  };
200
218
 
201
- this.get(basePath, { middleware }, requireMethod("index"));
219
+ this.get(basePath, routeOptions, requireMethod("index"));
202
220
  if (!options.apiOnly) {
203
- this.get(`${basePath}/create`, { middleware }, requireMethod("create"));
204
- this.get(`${itemPath}/edit`, { middleware }, requireMethod("edit"));
221
+ this.get(`${basePath}/create`, routeOptions, requireMethod("create"));
222
+ this.get(`${itemPath}/edit`, routeOptions, requireMethod("edit"));
205
223
  }
206
- this.post(basePath, { middleware }, requireMethod("store"));
207
- this.get(itemPath, { middleware }, requireMethod("show"));
208
- this.put(itemPath, { middleware }, requireMethod("update"));
209
- this.delete(itemPath, { middleware }, requireMethod("destroy"));
224
+ this.post(basePath, routeOptions, requireMethod("store"));
225
+ this.get(itemPath, routeOptions, requireMethod("show"));
226
+ this.put(itemPath, routeOptions, requireMethod("update"));
227
+ this.delete(itemPath, routeOptions, requireMethod("destroy"));
210
228
  }
211
229
 
212
230
  list() {
@@ -1,4 +1,4 @@
1
- import { normalizeObject } from "../../shared/support/normalize.js";
1
+ import { normalizeMobileConfig, normalizeObject, normalizeText } from "../../shared/support/normalize.js";
2
2
  import { normalizeSurfaceId } from "../../shared/surface/registry.js";
3
3
 
4
4
  function resolveAppConfig(scope = null) {
@@ -34,4 +34,95 @@ function resolveDefaultSurfaceId(scope = null, { defaultSurfaceId = "" } = {}) {
34
34
  });
35
35
  }
36
36
 
37
- export { resolveAppConfig, normalizeDefaultSurfaceId, resolveDefaultSurfaceId };
37
+ function resolveMobileConfig(source = null) {
38
+ const appConfig =
39
+ source && typeof source === "object" && typeof source.has === "function" && typeof source.make === "function"
40
+ ? resolveAppConfig(source)
41
+ : normalizeObject(source);
42
+
43
+ return normalizeMobileConfig(appConfig.mobile);
44
+ }
45
+
46
+ function resolveClientAssetMode(source = null) {
47
+ return resolveMobileConfig(source).assetMode;
48
+ }
49
+
50
+ function buildWebCallbackUrl(appPublicUrl = "", callbackPath = "") {
51
+ const normalizedAppPublicUrl = normalizeText(appPublicUrl);
52
+ const normalizedCallbackPath = normalizeText(callbackPath);
53
+ if (!normalizedAppPublicUrl || !normalizedCallbackPath) {
54
+ return "";
55
+ }
56
+
57
+ try {
58
+ const baseUrl = new URL(normalizedAppPublicUrl);
59
+ if (baseUrl.protocol !== "http:" && baseUrl.protocol !== "https:") {
60
+ return "";
61
+ }
62
+
63
+ if (!baseUrl.pathname.endsWith("/")) {
64
+ baseUrl.pathname = `${baseUrl.pathname}/`;
65
+ }
66
+ baseUrl.search = "";
67
+ baseUrl.hash = "";
68
+ const relativeCallbackPath = normalizedCallbackPath.startsWith("/")
69
+ ? normalizedCallbackPath.slice(1)
70
+ : normalizedCallbackPath;
71
+ return new URL(relativeCallbackPath, baseUrl).toString();
72
+ } catch {
73
+ return "";
74
+ }
75
+ }
76
+
77
+ function buildMobileCallbackUrl(customScheme = "", callbackPath = "") {
78
+ const normalizedScheme = normalizeText(customScheme).toLowerCase();
79
+ const normalizedCallbackPath = normalizeText(callbackPath);
80
+ if (!normalizedScheme || !normalizedCallbackPath) {
81
+ return "";
82
+ }
83
+
84
+ const suffix = normalizedCallbackPath.startsWith("/") ? normalizedCallbackPath.slice(1) : normalizedCallbackPath;
85
+ return suffix ? `${normalizedScheme}://${suffix}` : `${normalizedScheme}://`;
86
+ }
87
+
88
+ function buildAppLinkCallbackUrls(appLinkDomains = [], callbackPath = "") {
89
+ const normalizedCallbackPath = normalizeText(callbackPath);
90
+ if (!normalizedCallbackPath) {
91
+ return Object.freeze([]);
92
+ }
93
+
94
+ const urls = (Array.isArray(appLinkDomains) ? appLinkDomains : [])
95
+ .map((entry) => normalizeText(entry).toLowerCase())
96
+ .filter(Boolean)
97
+ .map((entry) => `https://${entry}${normalizedCallbackPath}`);
98
+
99
+ return Object.freeze([...new Set(urls)]);
100
+ }
101
+
102
+ function resolveMobileCallbackUrls(source = null, { appPublicUrl = "" } = {}) {
103
+ const mobileConfig = resolveMobileConfig(source);
104
+ const callbackPath = mobileConfig.auth.callbackPath;
105
+ const webCallbackUrl = buildWebCallbackUrl(appPublicUrl, callbackPath);
106
+ const mobileCallbackUrl = buildMobileCallbackUrl(mobileConfig.auth.customScheme, callbackPath);
107
+ const appLinkCallbackUrls = buildAppLinkCallbackUrls(mobileConfig.auth.appLinkDomains, callbackPath);
108
+ const callbackUrls = Object.freeze(
109
+ [...new Set([webCallbackUrl, mobileCallbackUrl, ...appLinkCallbackUrls].filter(Boolean))]
110
+ );
111
+
112
+ return Object.freeze({
113
+ callbackPath,
114
+ webCallbackUrl,
115
+ mobileCallbackUrl,
116
+ appLinkCallbackUrls,
117
+ callbackUrls
118
+ });
119
+ }
120
+
121
+ export {
122
+ resolveAppConfig,
123
+ normalizeDefaultSurfaceId,
124
+ resolveDefaultSurfaceId,
125
+ resolveMobileConfig,
126
+ resolveClientAssetMode,
127
+ resolveMobileCallbackUrls
128
+ };
@@ -2,8 +2,11 @@ import assert from "node:assert/strict";
2
2
  import test from "node:test";
3
3
  import {
4
4
  resolveAppConfig,
5
+ resolveClientAssetMode,
5
6
  normalizeDefaultSurfaceId,
6
- resolveDefaultSurfaceId
7
+ resolveDefaultSurfaceId,
8
+ resolveMobileCallbackUrls,
9
+ resolveMobileConfig
7
10
  } from "./appConfig.js";
8
11
 
9
12
  test("resolveAppConfig returns normalized appConfig when scope exposes appConfig binding", () => {
@@ -92,3 +95,101 @@ test("resolveDefaultSurfaceId falls back to appConfig and then empty default", (
92
95
  });
93
96
  assert.equal(fromKernelFallback, "");
94
97
  });
98
+
99
+ test("resolveMobileConfig reads the normalized mobile config from raw appConfig or scope", () => {
100
+ const fromRaw = resolveMobileConfig({
101
+ mobile: {
102
+ enabled: true,
103
+ strategy: "capacitor",
104
+ assetMode: "dev_server",
105
+ auth: {
106
+ customScheme: "convict"
107
+ }
108
+ }
109
+ });
110
+
111
+ assert.equal(fromRaw.enabled, true);
112
+ assert.equal(fromRaw.strategy, "capacitor");
113
+ assert.equal(fromRaw.assetMode, "dev_server");
114
+ assert.equal(fromRaw.auth.customScheme, "convict");
115
+
116
+ const fromScope = resolveMobileConfig({
117
+ has(token) {
118
+ return token === "appConfig";
119
+ },
120
+ make() {
121
+ return {
122
+ mobile: {
123
+ enabled: true,
124
+ strategy: "capacitor"
125
+ }
126
+ };
127
+ }
128
+ });
129
+ assert.equal(fromScope.enabled, true);
130
+ assert.equal(fromScope.strategy, "capacitor");
131
+ });
132
+
133
+ test("resolveClientAssetMode returns the normalized mobile asset mode", () => {
134
+ assert.equal(
135
+ resolveClientAssetMode({
136
+ mobile: {
137
+ assetMode: "dev_server"
138
+ }
139
+ }),
140
+ "dev_server"
141
+ );
142
+ assert.equal(resolveClientAssetMode({}), "bundled");
143
+ });
144
+
145
+ test("resolveMobileCallbackUrls resolves web, mobile, and app-link callback URLs", () => {
146
+ const callbackUrls = resolveMobileCallbackUrls(
147
+ {
148
+ mobile: {
149
+ auth: {
150
+ callbackPath: "/auth/login",
151
+ customScheme: "convict",
152
+ appLinkDomains: ["app.example.com", "APP.EXAMPLE.COM"]
153
+ }
154
+ }
155
+ },
156
+ {
157
+ appPublicUrl: "https://example.com/app"
158
+ }
159
+ );
160
+
161
+ assert.deepEqual(callbackUrls, {
162
+ callbackPath: "/auth/login",
163
+ webCallbackUrl: "https://example.com/app/auth/login",
164
+ mobileCallbackUrl: "convict://auth/login",
165
+ appLinkCallbackUrls: ["https://app.example.com/auth/login"],
166
+ callbackUrls: [
167
+ "https://example.com/app/auth/login",
168
+ "convict://auth/login",
169
+ "https://app.example.com/auth/login"
170
+ ]
171
+ });
172
+ });
173
+
174
+ test("resolveMobileCallbackUrls omits invalid or unavailable callback targets", () => {
175
+ const callbackUrls = resolveMobileCallbackUrls(
176
+ {
177
+ mobile: {
178
+ auth: {
179
+ callbackPath: "/auth/login"
180
+ }
181
+ }
182
+ },
183
+ {
184
+ appPublicUrl: "notaurl"
185
+ }
186
+ );
187
+
188
+ assert.deepEqual(callbackUrls, {
189
+ callbackPath: "/auth/login",
190
+ webCallbackUrl: "",
191
+ mobileCallbackUrl: "",
192
+ appLinkCallbackUrls: [],
193
+ callbackUrls: []
194
+ });
195
+ });
@@ -39,17 +39,20 @@ async function loadConfigModuleAtPath(absolutePath) {
39
39
  return normalizeConfigObject(loadedModule?.config);
40
40
  }
41
41
 
42
- async function loadAppConfigFromModuleUrl({
43
- moduleUrl = import.meta.url,
42
+ async function loadAppConfigFromAppRoot({
43
+ appRoot = "",
44
44
  publicConfigRelativePath = PUBLIC_CONFIG_RELATIVE_PATH,
45
45
  serverConfigRelativePath = SERVER_CONFIG_RELATIVE_PATH
46
46
  } = {}) {
47
- const appRoot = await resolveAppRootFromModuleUrl(moduleUrl, {
48
- publicConfigRelativePath
49
- });
47
+ const normalizedAppRootInput = String(appRoot || "").trim();
48
+ if (!normalizedAppRootInput) {
49
+ throw new Error("loadAppConfigFromAppRoot requires appRoot.");
50
+ }
51
+ const normalizedAppRoot = path.resolve(normalizedAppRootInput);
52
+
50
53
  const [publicConfig, serverConfig] = await Promise.all([
51
- loadConfigModuleAtPath(path.join(appRoot, publicConfigRelativePath)),
52
- loadConfigModuleAtPath(path.join(appRoot, serverConfigRelativePath))
54
+ loadConfigModuleAtPath(path.join(normalizedAppRoot, publicConfigRelativePath)),
55
+ loadConfigModuleAtPath(path.join(normalizedAppRoot, serverConfigRelativePath))
53
56
  ]);
54
57
 
55
58
  return Object.freeze({
@@ -58,4 +61,19 @@ async function loadAppConfigFromModuleUrl({
58
61
  });
59
62
  }
60
63
 
61
- export { loadAppConfigFromModuleUrl };
64
+ async function loadAppConfigFromModuleUrl({
65
+ moduleUrl = import.meta.url,
66
+ publicConfigRelativePath = PUBLIC_CONFIG_RELATIVE_PATH,
67
+ serverConfigRelativePath = SERVER_CONFIG_RELATIVE_PATH
68
+ } = {}) {
69
+ const appRoot = await resolveAppRootFromModuleUrl(moduleUrl, {
70
+ publicConfigRelativePath
71
+ });
72
+ return loadAppConfigFromAppRoot({
73
+ appRoot,
74
+ publicConfigRelativePath,
75
+ serverConfigRelativePath
76
+ });
77
+ }
78
+
79
+ export { loadAppConfigFromAppRoot, loadAppConfigFromModuleUrl };
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
  import os from "node:os";
5
5
  import test from "node:test";
6
6
  import { pathToFileURL } from "node:url";
7
- import { loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
7
+ import { loadAppConfigFromAppRoot, loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
8
8
 
9
9
  async function createModuleUrlAt(absolutePath) {
10
10
  await mkdir(path.dirname(absolutePath), { recursive: true });
@@ -30,6 +30,30 @@ test("loadAppConfigFromModuleUrl merges public and server config", async () => {
30
30
  assert.equal(Object.isFrozen(loaded), true);
31
31
  });
32
32
 
33
+ test("loadAppConfigFromAppRoot merges public and server config", async () => {
34
+ const tempRoot = await mkdtemp(path.join(os.tmpdir(), "kernel-app-config-"));
35
+ const appRoot = path.join(tempRoot, "app");
36
+ await mkdir(path.join(appRoot, "config"), { recursive: true });
37
+ await writeFile(path.join(appRoot, "config", "public.js"), "export const config = { a: 1, shared: 'public' };", "utf8");
38
+ await writeFile(path.join(appRoot, "config", "server.js"), "export const config = { b: 2, shared: 'server' };", "utf8");
39
+
40
+ const loaded = await loadAppConfigFromAppRoot({ appRoot });
41
+
42
+ assert.deepEqual(loaded, {
43
+ a: 1,
44
+ b: 2,
45
+ shared: "server"
46
+ });
47
+ assert.equal(Object.isFrozen(loaded), true);
48
+ });
49
+
50
+ test("loadAppConfigFromAppRoot requires an explicit appRoot", async () => {
51
+ await assert.rejects(
52
+ loadAppConfigFromAppRoot({ appRoot: "" }),
53
+ /requires appRoot/
54
+ );
55
+ });
56
+
33
57
  test("loadAppConfigFromModuleUrl tolerates missing server config", async () => {
34
58
  const tempRoot = await mkdtemp(path.join(os.tmpdir(), "kernel-app-config-"));
35
59
  const appRoot = path.join(tempRoot, "app");
@@ -1,6 +1,6 @@
1
1
  export { symlinkSafeRequire } from "./symlinkSafeRequire.js";
2
- export { resolveAppConfig } from "./appConfig.js";
3
- export { loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
2
+ export { resolveAppConfig, resolveMobileConfig, resolveClientAssetMode, resolveMobileCallbackUrls } from "./appConfig.js";
3
+ export { loadAppConfigFromAppRoot, loadAppConfigFromModuleUrl } from "./appConfigFiles.js";
4
4
  export { importFreshModuleFromAbsolutePath } from "./importFreshModuleFromAbsolutePath.js";
5
5
  export { resolveRequiredAppRoot, toPosixPath } from "./path.js";
6
6
  export {
@@ -254,6 +254,94 @@ function normalizeOneOf(value, allowedValues = [], fallback = "") {
254
254
  return supported[0] || "";
255
255
  }
256
256
 
257
+ const MOBILE_ASSET_MODES = Object.freeze(["bundled", "dev_server"]);
258
+ const MOBILE_STRATEGIES = Object.freeze(["capacitor"]);
259
+ const DEFAULT_MOBILE_AUTH_CONFIG = Object.freeze({
260
+ callbackPath: "/auth/login",
261
+ customScheme: "",
262
+ appLinkDomains: Object.freeze([])
263
+ });
264
+ const DEFAULT_MOBILE_ANDROID_CONFIG = Object.freeze({
265
+ packageName: "",
266
+ minSdk: 26,
267
+ targetSdk: 35,
268
+ versionCode: 1,
269
+ versionName: "1.0.0"
270
+ });
271
+
272
+ function normalizeMobileStrategy(value = "", { fallback = "" } = {}) {
273
+ const normalized = normalizeLowerText(value);
274
+ return MOBILE_STRATEGIES.includes(normalized) ? normalized : normalizeLowerText(fallback);
275
+ }
276
+
277
+ function normalizeMobileAssetMode(value = "", { fallback = "bundled" } = {}) {
278
+ const normalized = normalizeLowerText(value);
279
+ if (!normalized) {
280
+ return normalizeLowerText(fallback, {
281
+ fallback: "bundled"
282
+ });
283
+ }
284
+ if (!MOBILE_ASSET_MODES.includes(normalized)) {
285
+ throw new TypeError('config.mobile.assetMode must be "bundled" or "dev_server".');
286
+ }
287
+ return normalized;
288
+ }
289
+
290
+ function normalizeMobileCallbackPath(value = "", { fallback = "/auth/login" } = {}) {
291
+ const normalized = normalizeText(value, {
292
+ fallback
293
+ });
294
+ if (!normalized) {
295
+ return "";
296
+ }
297
+
298
+ return normalized.startsWith("/") ? normalized : `/${normalized}`;
299
+ }
300
+
301
+ function normalizeMobileConfig(source = {}) {
302
+ const mobile = normalizeObject(source);
303
+ const auth = normalizeObject(mobile.auth);
304
+ const android = normalizeObject(mobile.android);
305
+
306
+ return Object.freeze({
307
+ enabled: hasValue(mobile.enabled) ? normalizeBoolean(mobile.enabled) : false,
308
+ strategy: normalizeMobileStrategy(mobile.strategy),
309
+ appId: normalizeText(mobile.appId),
310
+ appName: normalizeText(mobile.appName),
311
+ assetMode: normalizeMobileAssetMode(mobile.assetMode),
312
+ devServerUrl: normalizeText(mobile.devServerUrl),
313
+ apiBaseUrl: normalizeText(mobile.apiBaseUrl),
314
+ auth: Object.freeze({
315
+ callbackPath: normalizeMobileCallbackPath(auth.callbackPath, {
316
+ fallback: DEFAULT_MOBILE_AUTH_CONFIG.callbackPath
317
+ }),
318
+ customScheme: normalizeLowerText(auth.customScheme),
319
+ appLinkDomains: Object.freeze([
320
+ ...new Set(
321
+ normalizeUniqueTextList(auth.appLinkDomains, {
322
+ acceptSingle: true
323
+ }).map((entry) => normalizeLowerText(entry))
324
+ )
325
+ ])
326
+ }),
327
+ android: Object.freeze({
328
+ packageName: normalizeText(android.packageName),
329
+ minSdk: normalizePositiveInteger(android.minSdk, {
330
+ fallback: DEFAULT_MOBILE_ANDROID_CONFIG.minSdk
331
+ }),
332
+ targetSdk: normalizePositiveInteger(android.targetSdk, {
333
+ fallback: DEFAULT_MOBILE_ANDROID_CONFIG.targetSdk
334
+ }),
335
+ versionCode: normalizePositiveInteger(android.versionCode, {
336
+ fallback: DEFAULT_MOBILE_ANDROID_CONFIG.versionCode
337
+ }),
338
+ versionName: normalizeText(android.versionName, {
339
+ fallback: DEFAULT_MOBILE_ANDROID_CONFIG.versionName
340
+ })
341
+ })
342
+ });
343
+ }
344
+
257
345
  function ensureNonEmptyText(value, label = "value") {
258
346
  const normalized = normalizeText(value);
259
347
  if (!normalized) {
@@ -283,5 +371,9 @@ export {
283
371
  normalizeRecordId,
284
372
  normalizeOpaqueId,
285
373
  normalizeOneOf,
374
+ normalizeMobileAssetMode,
375
+ normalizeMobileCallbackPath,
376
+ normalizeMobileConfig,
377
+ normalizeMobileStrategy,
286
378
  ensureNonEmptyText
287
379
  };