@aglyn/aglyn 1.0.0-beta.255 → 1.0.0-beta.256

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.
@@ -28,10 +28,12 @@ export declare const NATIVE_APP_ASSOCIATION_ROUTES: Readonly<Record<string, stri
28
28
  export declare function nativeAppAssociationRoute(pathname: string): string | null;
29
29
  /**
30
30
  * The console's top-level sections the native apps have no screen for, so a
31
- * link to one stays in the browser. They are the sign-in and verification
32
- * flows (an emailed verify or reset link opened by an app that cannot finish
33
- * it would strand the person), the sign-out page, the site admin bar's
34
- * connect page and a plugin's public device pages.
31
+ * link to one stays in the browser: the sign-in page and SSO (the system
32
+ * auth session opens them for the app, and a universal link there would loop
33
+ * back into the app), the native hand-off and other `/auth` pages, the
34
+ * sign-out page, the site admin bar's connect page and a plugin's public
35
+ * device pages. The sections the apps finish natively are
36
+ * {@link NATIVE_AUTH_LINK_SEGMENTS}.
35
37
  *
36
38
  * The rest of the console's top-level sections (`admin`, `billing`,
37
39
  * `manage`, `support`) and every workspace path are what the native
@@ -41,6 +43,14 @@ export declare function nativeAppAssociationRoute(pathname: string): string | nu
41
43
  * this list to the console's route set and the native link specs.
42
44
  */
43
45
  export declare const NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS: readonly string[];
46
+ /**
47
+ * The pre-sign-in sections whose exact page the apps finish natively: sign-up
48
+ * (with its `?plan=`), an emailed verification link, an emailed password
49
+ * reset and account recovery (`AuthFlow.swift`; Android's console intent
50
+ * filter lists the same four paths). Only the page itself opens the app;
51
+ * anything under it stays in the browser, where the console handles it.
52
+ */
53
+ export declare const NATIVE_AUTH_LINK_SEGMENTS: readonly string[];
44
54
  /** One `applinks` component: a URL path pattern, optionally excluded. */
45
55
  export interface AppLinksComponent {
46
56
  '/': string;
@@ -48,10 +48,12 @@
48
48
  }
49
49
  /**
50
50
  * The console's top-level sections the native apps have no screen for, so a
51
- * link to one stays in the browser. They are the sign-in and verification
52
- * flows (an emailed verify or reset link opened by an app that cannot finish
53
- * it would strand the person), the sign-out page, the site admin bar's
54
- * connect page and a plugin's public device pages.
51
+ * link to one stays in the browser: the sign-in page and SSO (the system
52
+ * auth session opens them for the app, and a universal link there would loop
53
+ * back into the app), the native hand-off and other `/auth` pages, the
54
+ * sign-out page, the site admin bar's connect page and a plugin's public
55
+ * device pages. The sections the apps finish natively are
56
+ * {@link NATIVE_AUTH_LINK_SEGMENTS}.
55
57
  *
56
58
  * The rest of the console's top-level sections (`admin`, `billing`,
57
59
  * `manage`, `support`) and every workspace path are what the native
@@ -60,15 +62,23 @@
60
62
  * native screen. `apps/console/specs/native-app-association.spec.ts` holds
61
63
  * this list to the console's route set and the native link specs.
62
64
  */ export const NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS = [
63
- 'account-recovery',
64
65
  'auth',
65
66
  'edit-access',
66
67
  'kiosk',
67
- 'reset-password',
68
68
  'signin',
69
69
  'signout',
70
+ 'sso'
71
+ ];
72
+ /**
73
+ * The pre-sign-in sections whose exact page the apps finish natively: sign-up
74
+ * (with its `?plan=`), an emailed verification link, an emailed password
75
+ * reset and account recovery (`AuthFlow.swift`; Android's console intent
76
+ * filter lists the same four paths). Only the page itself opens the app;
77
+ * anything under it stays in the browser, where the console handles it.
78
+ */ export const NATIVE_AUTH_LINK_SEGMENTS = [
79
+ 'account-recovery',
80
+ 'reset-password',
70
81
  'signup',
71
- 'sso',
72
82
  'verify-email'
73
83
  ];
74
84
  /**
@@ -105,6 +115,12 @@
105
115
  exclude: true
106
116
  });
107
117
  }
118
+ for (const segment of NATIVE_AUTH_LINK_SEGMENTS){
119
+ excluded.push({
120
+ '/': `/${segment}/*`,
121
+ exclude: true
122
+ });
123
+ }
108
124
  return [
109
125
  ...excluded,
110
126
  {
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/native-app-association.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * THE DOCUMENTS THAT TIE THE NATIVE APPS TO A DOMAIN (AGL-3817).\n *\n * Apple's `apple-app-site-association` and Google's `assetlinks.json` are how\n * a domain says which apps may use its passkeys (`webcredentials`,\n * `get_login_creds`) and open its links (`applinks`, `handle_all_urls`). Both\n * are fetched by the platform's verifier, not by a person, so each has to be\n * exactly `application/json`, a 200, no redirect and no cookie.\n *\n * Everything that names an app comes from the environment, never from here:\n *\n * - `NATIVE_APPLE_APP_IDS`: comma-separated `<team id>.<bundle id>` values,\n * in the order the system should prefer them;\n * - `NATIVE_ANDROID_APPS`: `;`-separated `<package>=<sha256>,<sha256>`\n * entries, each fingerprint the colon-separated hex Play Console prints.\n *\n * Unset (or nothing valid in it), the document answers 404: a deployment\n * with no apps of its own vouches for none.\n *\n * A leaf with no imports, because both middlewares read the paths below.\n *=========================================*/\n\n/** Where Apple's verifier reads the association file: no `.json` suffix. */\nexport const APPLE_APP_SITE_ASSOCIATION_PATH = '/.well-known/apple-app-site-association'\n\n/** Where Google's verifier reads the statement list. */\nexport const ASSET_LINKS_PATH = '/.well-known/assetlinks.json'\n\n/**\n * The route handlers each middleware rewrites the two public paths to. They\n * live under `/api` because both apps keep their route handlers there, and\n * neither middleware runs again on `/api`.\n */\nexport const NATIVE_APP_ASSOCIATION_ROUTES: Readonly<Record<string, string>> = {\n [APPLE_APP_SITE_ASSOCIATION_PATH]: '/api/native-app-association/apple-app-site-association',\n [ASSET_LINKS_PATH]: '/api/native-app-association/assetlinks',\n}\n\n/** The route handler a public association path is served by, or null. */\nexport function nativeAppAssociationRoute(pathname: string): string | null {\n return Object.prototype.hasOwnProperty.call(NATIVE_APP_ASSOCIATION_ROUTES, pathname)\n ? NATIVE_APP_ASSOCIATION_ROUTES[pathname]\n : null\n}\n\n/**\n * The console's top-level sections the native apps have no screen for, so a\n * link to one stays in the browser. They are the sign-in and verification\n * flows (an emailed verify or reset link opened by an app that cannot finish\n * it would strand the person), the sign-out page, the site admin bar's\n * connect page and a plugin's public device pages.\n *\n * The rest of the console's top-level sections (`admin`, `billing`,\n * `manage`, `support`) and every workspace path are what the native\n * deep-link grammar resolves (`DeepLinks.swift`, `DeepLinks.kt`): a\n * registered screen, the Besigner in the app's own web view, or the nearest\n * native screen. `apps/console/specs/native-app-association.spec.ts` holds\n * this list to the console's route set and the native link specs.\n */\nexport const NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS: readonly string[] = [\n 'account-recovery',\n 'auth',\n 'edit-access',\n 'kiosk',\n 'reset-password',\n 'signin',\n 'signout',\n 'signup',\n 'sso',\n 'verify-email',\n]\n\n/** One `applinks` component: a URL path pattern, optionally excluded. */\nexport interface AppLinksComponent {\n '/': string\n exclude?: true\n comment?: string\n}\n\n/**\n * The console paths the apps open, as `applinks` components. Apple reads\n * them in order and the first match wins, so every exclusion comes ahead of\n * the closing catch-all.\n */\nexport function consoleAppLinksComponents(): AppLinksComponent[] {\n const excluded: AppLinksComponent[] = [\n { '/': '/_*', exclude: true, comment: 'framework and internal paths' },\n { '/': '/api', exclude: true },\n { '/': '/api/*', exclude: true, comment: 'route handlers' },\n { '/': '/.well-known/*', exclude: true },\n ]\n for (const segment of NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS) {\n excluded.push({ '/': `/${segment}`, exclude: true }, { '/': `/${segment}/*`, exclude: true })\n }\n return [\n ...excluded,\n { '/': '/*', comment: 'workspace, site, account, billing, support and staff pages' },\n ]\n}\n\n/** `TEAMID1234.com.example.app`: a 10-character team id, then a bundle id. */\nconst APPLE_APP_ID = /^[A-Z0-9]{10}\\.[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+$/\n\n/** A Java package name: dot-separated identifiers, at least two. */\nconst ANDROID_PACKAGE = /^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$/\n\n/** A SHA-256 certificate fingerprint as 32 colon-separated hex pairs. */\nconst SHA256_FINGERPRINT = /^([0-9A-F]{2}:){31}[0-9A-F]{2}$/\n\n/** The Apple app ids in `NATIVE_APPLE_APP_IDS`, in order; invalid ones dropped. */\nexport function parseAppleAppIds(raw: string | undefined): string[] {\n const ids = (raw ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => APPLE_APP_ID.test(value))\n return [...new Set(ids)]\n}\n\n/** One Android app and the signing certificates it may be verified by. */\nexport interface AndroidAppAssociation {\n packageName: string\n sha256Fingerprints: string[]\n}\n\n/**\n * The Android apps in `NATIVE_ANDROID_APPS`. An entry without a valid package\n * or without one valid fingerprint is dropped whole: a statement with no\n * certificate verifies nothing.\n */\nexport function parseAndroidApps(raw: string | undefined): AndroidAppAssociation[] {\n const apps: AndroidAppAssociation[] = []\n for (const entry of (raw ?? '').split(';')) {\n const at = entry.indexOf('=')\n if (at < 0) continue\n const packageName = entry.slice(0, at).trim()\n if (!ANDROID_PACKAGE.test(packageName)) continue\n if (apps.some((app) => app.packageName === packageName)) continue\n const fingerprints = entry\n .slice(at + 1)\n .split(',')\n .map((value) => value.trim().toUpperCase())\n .filter((value) => SHA256_FINGERPRINT.test(value))\n if (fingerprints.length === 0) continue\n apps.push({ packageName, sha256Fingerprints: [...new Set(fingerprints)] })\n }\n return apps\n}\n\n/** The `apple-app-site-association` document Apple's CDN reads. */\nexport interface AppleAppSiteAssociation {\n applinks?: {\n apps: []\n details: Array<{ appIDs: string[]; components: AppLinksComponent[] }>\n }\n webcredentials: { apps: string[] }\n}\n\n/**\n * The association file for these app ids. `applinks` is only for a host\n * whose paths the native deep-link grammar resolves: the console. A host\n * that serves the passkey relying party alone gets `webcredentials` only.\n *\n * One `details` entry per app, in the configured order, because when two\n * installed apps claim the same path the system takes the first.\n */\nexport function appleAppSiteAssociation(\n appIds: string[],\n options: { applinks: boolean },\n): AppleAppSiteAssociation {\n const document: AppleAppSiteAssociation = { webcredentials: { apps: [...appIds] } }\n if (options.applinks) {\n const components = consoleAppLinksComponents()\n document.applinks = {\n apps: [],\n details: appIds.map((appId) => ({ appIDs: [appId], components })),\n }\n }\n return document\n}\n\n/** One Digital Asset Links statement. */\nexport interface AssetLinkStatement {\n relation: string[]\n target: { namespace: 'android_app'; package_name: string; sha256_cert_fingerprints: string[] }\n}\n\n/** Passkeys and app links, for every configured app. */\nexport const ASSET_LINK_RELATIONS = [\n 'delegate_permission/common.handle_all_urls',\n 'delegate_permission/common.get_login_creds',\n] as const\n\n/** The `assetlinks.json` statement list Google's verifier reads. */\nexport function assetLinks(apps: AndroidAppAssociation[]): AssetLinkStatement[] {\n return apps.map((app) => ({\n relation: [...ASSET_LINK_RELATIONS],\n target: {\n namespace: 'android_app',\n package_name: app.packageName,\n sha256_cert_fingerprints: [...app.sha256Fingerprints],\n },\n }))\n}\n\n/** The request's hostname: lowercase, no port. */\nexport function requestHostname(host: string | null | undefined): string {\n return (host ?? '').trim().toLowerCase().replace(/:\\d+$/, '')\n}\n\n/** A URL's hostname, or null when it is not one. */\nexport function hostnameOfUrl(url: string | null | undefined): string | null {\n if (!url) return null\n try {\n return new URL(url).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/** What a route handler needs to answer one of the two documents. */\nexport interface NativeAppAssociationRequest {\n /** The request's `Host` header. */\n host: string | null\n /**\n * The hostnames this deployment vouches for. Every other host an app\n * serves (a customer's custom domain, a workspace subdomain, a white-label\n * console) answers 404, because the apps are this deployment's and not\n * theirs.\n */\n servedHosts: ReadonlyArray<string | null | undefined>\n /** Whether this host's paths are console links the apps resolve. */\n applinks: boolean\n /** The environment; `process.env` in the routes, a fixture in specs. */\n env: Readonly<Record<string, string | undefined>>\n}\n\n/** Short on purpose: Apple's CDN caches on top, and a fix must land within minutes. */\nconst CACHE_CONTROL = 'public, max-age=300, s-maxage=300'\n\n/**\n * Whether this host may answer. A Vercel preview deployment is reachable only\n * at the platform's own generated hostnames, so it answers on any host, which\n * is what lets a preview be checked before it is promoted.\n */\nfunction servesHost(request: NativeAppAssociationRequest): boolean {\n if (request.env['VERCEL_ENV'] === 'preview') return true\n const hostname = requestHostname(request.host)\n if (!hostname) return false\n return request.servedHosts.some((served) => requestHostname(served) === hostname)\n}\n\nfunction notFound(): Response {\n return new Response('Not Found', {\n status: 404,\n headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': CACHE_CONTROL },\n })\n}\n\nfunction json(body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: { 'content-type': 'application/json', 'cache-control': CACHE_CONTROL },\n })\n}\n\n/** `GET /.well-known/apple-app-site-association`. */\nexport function appleAppSiteAssociationResponse(request: NativeAppAssociationRequest): Response {\n const appIds = parseAppleAppIds(request.env['NATIVE_APPLE_APP_IDS'])\n if (appIds.length === 0 || !servesHost(request)) return notFound()\n return json(appleAppSiteAssociation(appIds, { applinks: request.applinks }))\n}\n\n/** `GET /.well-known/assetlinks.json`. */\nexport function assetLinksResponse(request: NativeAppAssociationRequest): Response {\n const apps = parseAndroidApps(request.env['NATIVE_ANDROID_APPS'])\n if (apps.length === 0 || !servesHost(request)) return notFound()\n return json(assetLinks(apps))\n}\n"],"names":["APPLE_APP_SITE_ASSOCIATION_PATH","ASSET_LINKS_PATH","NATIVE_APP_ASSOCIATION_ROUTES","nativeAppAssociationRoute","pathname","Object","prototype","hasOwnProperty","call","NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS","consoleAppLinksComponents","excluded","exclude","comment","segment","push","APPLE_APP_ID","ANDROID_PACKAGE","SHA256_FINGERPRINT","parseAppleAppIds","raw","ids","split","map","value","trim","filter","test","Set","parseAndroidApps","apps","entry","at","indexOf","packageName","slice","some","app","fingerprints","toUpperCase","length","sha256Fingerprints","appleAppSiteAssociation","appIds","options","document","webcredentials","applinks","components","details","appId","appIDs","ASSET_LINK_RELATIONS","assetLinks","relation","target","namespace","package_name","sha256_cert_fingerprints","requestHostname","host","toLowerCase","replace","hostnameOfUrl","url","URL","hostname","CACHE_CONTROL","servesHost","request","env","servedHosts","served","notFound","Response","status","headers","json","body","JSON","stringify","appleAppSiteAssociationResponse","assetLinksResponse"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/native-app-association.ts"],"sourcesContent":["/**\n * @license\n * Copyright 2026 Aglyn LLC\n *\n * Licensed under the Apache License, Version 2.0 (the \"License\");\n * you may not use this file except in compliance with the License.\n * You may obtain a copy of the License at\n *\n * http://www.apache.org/licenses/LICENSE-2.0\n *\n * Unless required by applicable law or agreed to in writing, software\n * distributed under the License is distributed on an \"AS IS\" BASIS,\n * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\n * See the License for the specific language governing permissions and\n * limitations under the License.\n */\n\n/*==========================================\n * THE DOCUMENTS THAT TIE THE NATIVE APPS TO A DOMAIN (AGL-3817).\n *\n * Apple's `apple-app-site-association` and Google's `assetlinks.json` are how\n * a domain says which apps may use its passkeys (`webcredentials`,\n * `get_login_creds`) and open its links (`applinks`, `handle_all_urls`). Both\n * are fetched by the platform's verifier, not by a person, so each has to be\n * exactly `application/json`, a 200, no redirect and no cookie.\n *\n * Everything that names an app comes from the environment, never from here:\n *\n * - `NATIVE_APPLE_APP_IDS`: comma-separated `<team id>.<bundle id>` values,\n * in the order the system should prefer them;\n * - `NATIVE_ANDROID_APPS`: `;`-separated `<package>=<sha256>,<sha256>`\n * entries, each fingerprint the colon-separated hex Play Console prints.\n *\n * Unset (or nothing valid in it), the document answers 404: a deployment\n * with no apps of its own vouches for none.\n *\n * A leaf with no imports, because both middlewares read the paths below.\n *=========================================*/\n\n/** Where Apple's verifier reads the association file: no `.json` suffix. */\nexport const APPLE_APP_SITE_ASSOCIATION_PATH = '/.well-known/apple-app-site-association'\n\n/** Where Google's verifier reads the statement list. */\nexport const ASSET_LINKS_PATH = '/.well-known/assetlinks.json'\n\n/**\n * The route handlers each middleware rewrites the two public paths to. They\n * live under `/api` because both apps keep their route handlers there, and\n * neither middleware runs again on `/api`.\n */\nexport const NATIVE_APP_ASSOCIATION_ROUTES: Readonly<Record<string, string>> = {\n [APPLE_APP_SITE_ASSOCIATION_PATH]: '/api/native-app-association/apple-app-site-association',\n [ASSET_LINKS_PATH]: '/api/native-app-association/assetlinks',\n}\n\n/** The route handler a public association path is served by, or null. */\nexport function nativeAppAssociationRoute(pathname: string): string | null {\n return Object.prototype.hasOwnProperty.call(NATIVE_APP_ASSOCIATION_ROUTES, pathname)\n ? NATIVE_APP_ASSOCIATION_ROUTES[pathname]\n : null\n}\n\n/**\n * The console's top-level sections the native apps have no screen for, so a\n * link to one stays in the browser: the sign-in page and SSO (the system\n * auth session opens them for the app, and a universal link there would loop\n * back into the app), the native hand-off and other `/auth` pages, the\n * sign-out page, the site admin bar's connect page and a plugin's public\n * device pages. The sections the apps finish natively are\n * {@link NATIVE_AUTH_LINK_SEGMENTS}.\n *\n * The rest of the console's top-level sections (`admin`, `billing`,\n * `manage`, `support`) and every workspace path are what the native\n * deep-link grammar resolves (`DeepLinks.swift`, `DeepLinks.kt`): a\n * registered screen, the Besigner in the app's own web view, or the nearest\n * native screen. `apps/console/specs/native-app-association.spec.ts` holds\n * this list to the console's route set and the native link specs.\n */\nexport const NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS: readonly string[] = [\n 'auth',\n 'edit-access',\n 'kiosk',\n 'signin',\n 'signout',\n 'sso',\n]\n\n/**\n * The pre-sign-in sections whose exact page the apps finish natively: sign-up\n * (with its `?plan=`), an emailed verification link, an emailed password\n * reset and account recovery (`AuthFlow.swift`; Android's console intent\n * filter lists the same four paths). Only the page itself opens the app;\n * anything under it stays in the browser, where the console handles it.\n */\nexport const NATIVE_AUTH_LINK_SEGMENTS: readonly string[] = [\n 'account-recovery',\n 'reset-password',\n 'signup',\n 'verify-email',\n]\n\n/** One `applinks` component: a URL path pattern, optionally excluded. */\nexport interface AppLinksComponent {\n '/': string\n exclude?: true\n comment?: string\n}\n\n/**\n * The console paths the apps open, as `applinks` components. Apple reads\n * them in order and the first match wins, so every exclusion comes ahead of\n * the closing catch-all.\n */\nexport function consoleAppLinksComponents(): AppLinksComponent[] {\n const excluded: AppLinksComponent[] = [\n { '/': '/_*', exclude: true, comment: 'framework and internal paths' },\n { '/': '/api', exclude: true },\n { '/': '/api/*', exclude: true, comment: 'route handlers' },\n { '/': '/.well-known/*', exclude: true },\n ]\n for (const segment of NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS) {\n excluded.push({ '/': `/${segment}`, exclude: true }, { '/': `/${segment}/*`, exclude: true })\n }\n for (const segment of NATIVE_AUTH_LINK_SEGMENTS) {\n excluded.push({ '/': `/${segment}/*`, exclude: true })\n }\n return [\n ...excluded,\n { '/': '/*', comment: 'workspace, site, account, billing, support and staff pages' },\n ]\n}\n\n/** `TEAMID1234.com.example.app`: a 10-character team id, then a bundle id. */\nconst APPLE_APP_ID = /^[A-Z0-9]{10}\\.[A-Za-z0-9-]+(\\.[A-Za-z0-9-]+)+$/\n\n/** A Java package name: dot-separated identifiers, at least two. */\nconst ANDROID_PACKAGE = /^[A-Za-z][A-Za-z0-9_]*(\\.[A-Za-z][A-Za-z0-9_]*)+$/\n\n/** A SHA-256 certificate fingerprint as 32 colon-separated hex pairs. */\nconst SHA256_FINGERPRINT = /^([0-9A-F]{2}:){31}[0-9A-F]{2}$/\n\n/** The Apple app ids in `NATIVE_APPLE_APP_IDS`, in order; invalid ones dropped. */\nexport function parseAppleAppIds(raw: string | undefined): string[] {\n const ids = (raw ?? '')\n .split(',')\n .map((value) => value.trim())\n .filter((value) => APPLE_APP_ID.test(value))\n return [...new Set(ids)]\n}\n\n/** One Android app and the signing certificates it may be verified by. */\nexport interface AndroidAppAssociation {\n packageName: string\n sha256Fingerprints: string[]\n}\n\n/**\n * The Android apps in `NATIVE_ANDROID_APPS`. An entry without a valid package\n * or without one valid fingerprint is dropped whole: a statement with no\n * certificate verifies nothing.\n */\nexport function parseAndroidApps(raw: string | undefined): AndroidAppAssociation[] {\n const apps: AndroidAppAssociation[] = []\n for (const entry of (raw ?? '').split(';')) {\n const at = entry.indexOf('=')\n if (at < 0) continue\n const packageName = entry.slice(0, at).trim()\n if (!ANDROID_PACKAGE.test(packageName)) continue\n if (apps.some((app) => app.packageName === packageName)) continue\n const fingerprints = entry\n .slice(at + 1)\n .split(',')\n .map((value) => value.trim().toUpperCase())\n .filter((value) => SHA256_FINGERPRINT.test(value))\n if (fingerprints.length === 0) continue\n apps.push({ packageName, sha256Fingerprints: [...new Set(fingerprints)] })\n }\n return apps\n}\n\n/** The `apple-app-site-association` document Apple's CDN reads. */\nexport interface AppleAppSiteAssociation {\n applinks?: {\n apps: []\n details: Array<{ appIDs: string[]; components: AppLinksComponent[] }>\n }\n webcredentials: { apps: string[] }\n}\n\n/**\n * The association file for these app ids. `applinks` is only for a host\n * whose paths the native deep-link grammar resolves: the console. A host\n * that serves the passkey relying party alone gets `webcredentials` only.\n *\n * One `details` entry per app, in the configured order, because when two\n * installed apps claim the same path the system takes the first.\n */\nexport function appleAppSiteAssociation(\n appIds: string[],\n options: { applinks: boolean },\n): AppleAppSiteAssociation {\n const document: AppleAppSiteAssociation = { webcredentials: { apps: [...appIds] } }\n if (options.applinks) {\n const components = consoleAppLinksComponents()\n document.applinks = {\n apps: [],\n details: appIds.map((appId) => ({ appIDs: [appId], components })),\n }\n }\n return document\n}\n\n/** One Digital Asset Links statement. */\nexport interface AssetLinkStatement {\n relation: string[]\n target: { namespace: 'android_app'; package_name: string; sha256_cert_fingerprints: string[] }\n}\n\n/** Passkeys and app links, for every configured app. */\nexport const ASSET_LINK_RELATIONS = [\n 'delegate_permission/common.handle_all_urls',\n 'delegate_permission/common.get_login_creds',\n] as const\n\n/** The `assetlinks.json` statement list Google's verifier reads. */\nexport function assetLinks(apps: AndroidAppAssociation[]): AssetLinkStatement[] {\n return apps.map((app) => ({\n relation: [...ASSET_LINK_RELATIONS],\n target: {\n namespace: 'android_app',\n package_name: app.packageName,\n sha256_cert_fingerprints: [...app.sha256Fingerprints],\n },\n }))\n}\n\n/** The request's hostname: lowercase, no port. */\nexport function requestHostname(host: string | null | undefined): string {\n return (host ?? '').trim().toLowerCase().replace(/:\\d+$/, '')\n}\n\n/** A URL's hostname, or null when it is not one. */\nexport function hostnameOfUrl(url: string | null | undefined): string | null {\n if (!url) return null\n try {\n return new URL(url).hostname.toLowerCase() || null\n } catch {\n return null\n }\n}\n\n/** What a route handler needs to answer one of the two documents. */\nexport interface NativeAppAssociationRequest {\n /** The request's `Host` header. */\n host: string | null\n /**\n * The hostnames this deployment vouches for. Every other host an app\n * serves (a customer's custom domain, a workspace subdomain, a white-label\n * console) answers 404, because the apps are this deployment's and not\n * theirs.\n */\n servedHosts: ReadonlyArray<string | null | undefined>\n /** Whether this host's paths are console links the apps resolve. */\n applinks: boolean\n /** The environment; `process.env` in the routes, a fixture in specs. */\n env: Readonly<Record<string, string | undefined>>\n}\n\n/** Short on purpose: Apple's CDN caches on top, and a fix must land within minutes. */\nconst CACHE_CONTROL = 'public, max-age=300, s-maxage=300'\n\n/**\n * Whether this host may answer. A Vercel preview deployment is reachable only\n * at the platform's own generated hostnames, so it answers on any host, which\n * is what lets a preview be checked before it is promoted.\n */\nfunction servesHost(request: NativeAppAssociationRequest): boolean {\n if (request.env['VERCEL_ENV'] === 'preview') return true\n const hostname = requestHostname(request.host)\n if (!hostname) return false\n return request.servedHosts.some((served) => requestHostname(served) === hostname)\n}\n\nfunction notFound(): Response {\n return new Response('Not Found', {\n status: 404,\n headers: { 'content-type': 'text/plain; charset=utf-8', 'cache-control': CACHE_CONTROL },\n })\n}\n\nfunction json(body: unknown): Response {\n return new Response(JSON.stringify(body), {\n status: 200,\n headers: { 'content-type': 'application/json', 'cache-control': CACHE_CONTROL },\n })\n}\n\n/** `GET /.well-known/apple-app-site-association`. */\nexport function appleAppSiteAssociationResponse(request: NativeAppAssociationRequest): Response {\n const appIds = parseAppleAppIds(request.env['NATIVE_APPLE_APP_IDS'])\n if (appIds.length === 0 || !servesHost(request)) return notFound()\n return json(appleAppSiteAssociation(appIds, { applinks: request.applinks }))\n}\n\n/** `GET /.well-known/assetlinks.json`. */\nexport function assetLinksResponse(request: NativeAppAssociationRequest): Response {\n const apps = parseAndroidApps(request.env['NATIVE_ANDROID_APPS'])\n if (apps.length === 0 || !servesHost(request)) return notFound()\n return json(assetLinks(apps))\n}\n"],"names":["APPLE_APP_SITE_ASSOCIATION_PATH","ASSET_LINKS_PATH","NATIVE_APP_ASSOCIATION_ROUTES","nativeAppAssociationRoute","pathname","Object","prototype","hasOwnProperty","call","NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS","NATIVE_AUTH_LINK_SEGMENTS","consoleAppLinksComponents","excluded","exclude","comment","segment","push","APPLE_APP_ID","ANDROID_PACKAGE","SHA256_FINGERPRINT","parseAppleAppIds","raw","ids","split","map","value","trim","filter","test","Set","parseAndroidApps","apps","entry","at","indexOf","packageName","slice","some","app","fingerprints","toUpperCase","length","sha256Fingerprints","appleAppSiteAssociation","appIds","options","document","webcredentials","applinks","components","details","appId","appIDs","ASSET_LINK_RELATIONS","assetLinks","relation","target","namespace","package_name","sha256_cert_fingerprints","requestHostname","host","toLowerCase","replace","hostnameOfUrl","url","URL","hostname","CACHE_CONTROL","servesHost","request","env","servedHosts","served","notFound","Response","status","headers","json","body","JSON","stringify","appleAppSiteAssociationResponse","assetLinksResponse"],"mappings":"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"}
@@ -241,9 +241,9 @@
241
241
  "id": "inventory-sync",
242
242
  "label": "Inventory sync",
243
243
  "alwaysOnForWorkspace": true,
244
- "description": "Stock, products and paid orders kept in step with your own Cin7 Core, inFlow or Brightpearl account.",
244
+ "description": "Stock, products and paid orders kept in step with your own Cin7 Core, inFlow, Brightpearl or Ordoro account.",
245
245
  "siteOff": {
246
- "stops": "Switching Inventory sync off for this site stops its stock counts, products and paid orders syncing with Cin7 Core, inFlow or Brightpearl.",
246
+ "stops": "Switching Inventory sync off for this site stops its stock counts, products and paid orders syncing with Cin7 Core, inFlow, Brightpearl or Ordoro.",
247
247
  "keeps": "The connection and its settings are kept, and everything already synced stays on both sides."
248
248
  }
249
249
  },