@aglyn/aglyn 1.0.0-beta.254 → 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.
- package/package.json +11 -11
- package/src/lib/app-utils/activity-labels.js +2 -0
- package/src/lib/app-utils/activity-labels.js.map +1 -1
- package/src/lib/app-utils/business-profile-values.d.ts +83 -0
- package/src/lib/app-utils/business-profile-values.js +160 -0
- package/src/lib/app-utils/business-profile-values.js.map +1 -0
- package/src/lib/app-utils/business-profile.d.ts +7 -60
- package/src/lib/app-utils/business-profile.js +2 -172
- package/src/lib/app-utils/business-profile.js.map +1 -1
- package/src/lib/app-utils/docs-help-sections.generated.js +8 -5
- package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
- package/src/lib/app-utils/docs-help.generated.d.ts +16 -10
- package/src/lib/app-utils/docs-help.generated.js +56 -18
- package/src/lib/app-utils/docs-help.generated.js.map +1 -1
- package/src/lib/app-utils/docs-index.generated.js +278 -5
- package/src/lib/app-utils/docs-index.generated.js.map +1 -1
- package/src/lib/app-utils/lockdown.d.ts +6 -0
- package/src/lib/app-utils/lockdown.js +20 -0
- package/src/lib/app-utils/lockdown.js.map +1 -1
- package/src/lib/app-utils/native-app-association.d.ts +139 -0
- package/src/lib/app-utils/native-app-association.js +264 -0
- package/src/lib/app-utils/native-app-association.js.map +1 -0
- package/src/lib/app-utils/sso-enforcement.d.ts +69 -0
- package/src/lib/app-utils/sso-enforcement.js +66 -0
- package/src/lib/app-utils/sso-enforcement.js.map +1 -0
- package/src/lib/app-utils/time-zone-choices.d.ts +46 -0
- package/src/lib/app-utils/time-zone-choices.js +469 -0
- package/src/lib/app-utils/time-zone-choices.js.map +1 -0
- package/src/lib/data-transfer/site-package-diff.d.ts +108 -0
- package/src/lib/data-transfer/site-package-diff.js +268 -0
- package/src/lib/data-transfer/site-package-diff.js.map +1 -0
- package/src/lib/plugin-manager/first-party-plugins.generated.js +2 -2
- package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/** Where Apple's verifier reads the association file: no `.json` suffix. */
|
|
18
|
+
export declare const APPLE_APP_SITE_ASSOCIATION_PATH = "/.well-known/apple-app-site-association";
|
|
19
|
+
/** Where Google's verifier reads the statement list. */
|
|
20
|
+
export declare const ASSET_LINKS_PATH = "/.well-known/assetlinks.json";
|
|
21
|
+
/**
|
|
22
|
+
* The route handlers each middleware rewrites the two public paths to. They
|
|
23
|
+
* live under `/api` because both apps keep their route handlers there, and
|
|
24
|
+
* neither middleware runs again on `/api`.
|
|
25
|
+
*/
|
|
26
|
+
export declare const NATIVE_APP_ASSOCIATION_ROUTES: Readonly<Record<string, string>>;
|
|
27
|
+
/** The route handler a public association path is served by, or null. */
|
|
28
|
+
export declare function nativeAppAssociationRoute(pathname: string): string | null;
|
|
29
|
+
/**
|
|
30
|
+
* The console's top-level sections the native apps have no screen for, so a
|
|
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}.
|
|
37
|
+
*
|
|
38
|
+
* The rest of the console's top-level sections (`admin`, `billing`,
|
|
39
|
+
* `manage`, `support`) and every workspace path are what the native
|
|
40
|
+
* deep-link grammar resolves (`DeepLinks.swift`, `DeepLinks.kt`): a
|
|
41
|
+
* registered screen, the Besigner in the app's own web view, or the nearest
|
|
42
|
+
* native screen. `apps/console/specs/native-app-association.spec.ts` holds
|
|
43
|
+
* this list to the console's route set and the native link specs.
|
|
44
|
+
*/
|
|
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[];
|
|
54
|
+
/** One `applinks` component: a URL path pattern, optionally excluded. */
|
|
55
|
+
export interface AppLinksComponent {
|
|
56
|
+
'/': string;
|
|
57
|
+
exclude?: true;
|
|
58
|
+
comment?: string;
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The console paths the apps open, as `applinks` components. Apple reads
|
|
62
|
+
* them in order and the first match wins, so every exclusion comes ahead of
|
|
63
|
+
* the closing catch-all.
|
|
64
|
+
*/
|
|
65
|
+
export declare function consoleAppLinksComponents(): AppLinksComponent[];
|
|
66
|
+
/** The Apple app ids in `NATIVE_APPLE_APP_IDS`, in order; invalid ones dropped. */
|
|
67
|
+
export declare function parseAppleAppIds(raw: string | undefined): string[];
|
|
68
|
+
/** One Android app and the signing certificates it may be verified by. */
|
|
69
|
+
export interface AndroidAppAssociation {
|
|
70
|
+
packageName: string;
|
|
71
|
+
sha256Fingerprints: string[];
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* The Android apps in `NATIVE_ANDROID_APPS`. An entry without a valid package
|
|
75
|
+
* or without one valid fingerprint is dropped whole: a statement with no
|
|
76
|
+
* certificate verifies nothing.
|
|
77
|
+
*/
|
|
78
|
+
export declare function parseAndroidApps(raw: string | undefined): AndroidAppAssociation[];
|
|
79
|
+
/** The `apple-app-site-association` document Apple's CDN reads. */
|
|
80
|
+
export interface AppleAppSiteAssociation {
|
|
81
|
+
applinks?: {
|
|
82
|
+
apps: [];
|
|
83
|
+
details: Array<{
|
|
84
|
+
appIDs: string[];
|
|
85
|
+
components: AppLinksComponent[];
|
|
86
|
+
}>;
|
|
87
|
+
};
|
|
88
|
+
webcredentials: {
|
|
89
|
+
apps: string[];
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* The association file for these app ids. `applinks` is only for a host
|
|
94
|
+
* whose paths the native deep-link grammar resolves: the console. A host
|
|
95
|
+
* that serves the passkey relying party alone gets `webcredentials` only.
|
|
96
|
+
*
|
|
97
|
+
* One `details` entry per app, in the configured order, because when two
|
|
98
|
+
* installed apps claim the same path the system takes the first.
|
|
99
|
+
*/
|
|
100
|
+
export declare function appleAppSiteAssociation(appIds: string[], options: {
|
|
101
|
+
applinks: boolean;
|
|
102
|
+
}): AppleAppSiteAssociation;
|
|
103
|
+
/** One Digital Asset Links statement. */
|
|
104
|
+
export interface AssetLinkStatement {
|
|
105
|
+
relation: string[];
|
|
106
|
+
target: {
|
|
107
|
+
namespace: 'android_app';
|
|
108
|
+
package_name: string;
|
|
109
|
+
sha256_cert_fingerprints: string[];
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
/** Passkeys and app links, for every configured app. */
|
|
113
|
+
export declare const ASSET_LINK_RELATIONS: readonly ["delegate_permission/common.handle_all_urls", "delegate_permission/common.get_login_creds"];
|
|
114
|
+
/** The `assetlinks.json` statement list Google's verifier reads. */
|
|
115
|
+
export declare function assetLinks(apps: AndroidAppAssociation[]): AssetLinkStatement[];
|
|
116
|
+
/** The request's hostname: lowercase, no port. */
|
|
117
|
+
export declare function requestHostname(host: string | null | undefined): string;
|
|
118
|
+
/** A URL's hostname, or null when it is not one. */
|
|
119
|
+
export declare function hostnameOfUrl(url: string | null | undefined): string | null;
|
|
120
|
+
/** What a route handler needs to answer one of the two documents. */
|
|
121
|
+
export interface NativeAppAssociationRequest {
|
|
122
|
+
/** The request's `Host` header. */
|
|
123
|
+
host: string | null;
|
|
124
|
+
/**
|
|
125
|
+
* The hostnames this deployment vouches for. Every other host an app
|
|
126
|
+
* serves (a customer's custom domain, a workspace subdomain, a white-label
|
|
127
|
+
* console) answers 404, because the apps are this deployment's and not
|
|
128
|
+
* theirs.
|
|
129
|
+
*/
|
|
130
|
+
servedHosts: ReadonlyArray<string | null | undefined>;
|
|
131
|
+
/** Whether this host's paths are console links the apps resolve. */
|
|
132
|
+
applinks: boolean;
|
|
133
|
+
/** The environment; `process.env` in the routes, a fixture in specs. */
|
|
134
|
+
env: Readonly<Record<string, string | undefined>>;
|
|
135
|
+
}
|
|
136
|
+
/** `GET /.well-known/apple-app-site-association`. */
|
|
137
|
+
export declare function appleAppSiteAssociationResponse(request: NativeAppAssociationRequest): Response;
|
|
138
|
+
/** `GET /.well-known/assetlinks.json`. */
|
|
139
|
+
export declare function assetLinksResponse(request: NativeAppAssociationRequest): Response;
|
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /*==========================================
|
|
17
|
+
* THE DOCUMENTS THAT TIE THE NATIVE APPS TO A DOMAIN (AGL-3817).
|
|
18
|
+
*
|
|
19
|
+
* Apple's `apple-app-site-association` and Google's `assetlinks.json` are how
|
|
20
|
+
* a domain says which apps may use its passkeys (`webcredentials`,
|
|
21
|
+
* `get_login_creds`) and open its links (`applinks`, `handle_all_urls`). Both
|
|
22
|
+
* are fetched by the platform's verifier, not by a person, so each has to be
|
|
23
|
+
* exactly `application/json`, a 200, no redirect and no cookie.
|
|
24
|
+
*
|
|
25
|
+
* Everything that names an app comes from the environment, never from here:
|
|
26
|
+
*
|
|
27
|
+
* - `NATIVE_APPLE_APP_IDS`: comma-separated `<team id>.<bundle id>` values,
|
|
28
|
+
* in the order the system should prefer them;
|
|
29
|
+
* - `NATIVE_ANDROID_APPS`: `;`-separated `<package>=<sha256>,<sha256>`
|
|
30
|
+
* entries, each fingerprint the colon-separated hex Play Console prints.
|
|
31
|
+
*
|
|
32
|
+
* Unset (or nothing valid in it), the document answers 404: a deployment
|
|
33
|
+
* with no apps of its own vouches for none.
|
|
34
|
+
*
|
|
35
|
+
* A leaf with no imports, because both middlewares read the paths below.
|
|
36
|
+
*=========================================*/ /** Where Apple's verifier reads the association file: no `.json` suffix. */ export const APPLE_APP_SITE_ASSOCIATION_PATH = '/.well-known/apple-app-site-association';
|
|
37
|
+
/** Where Google's verifier reads the statement list. */ export const ASSET_LINKS_PATH = '/.well-known/assetlinks.json';
|
|
38
|
+
/**
|
|
39
|
+
* The route handlers each middleware rewrites the two public paths to. They
|
|
40
|
+
* live under `/api` because both apps keep their route handlers there, and
|
|
41
|
+
* neither middleware runs again on `/api`.
|
|
42
|
+
*/ export const NATIVE_APP_ASSOCIATION_ROUTES = {
|
|
43
|
+
[APPLE_APP_SITE_ASSOCIATION_PATH]: '/api/native-app-association/apple-app-site-association',
|
|
44
|
+
[ASSET_LINKS_PATH]: '/api/native-app-association/assetlinks'
|
|
45
|
+
};
|
|
46
|
+
/** The route handler a public association path is served by, or null. */ export function nativeAppAssociationRoute(pathname) {
|
|
47
|
+
return Object.prototype.hasOwnProperty.call(NATIVE_APP_ASSOCIATION_ROUTES, pathname) ? NATIVE_APP_ASSOCIATION_ROUTES[pathname] : null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* The console's top-level sections the native apps have no screen for, so a
|
|
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}.
|
|
57
|
+
*
|
|
58
|
+
* The rest of the console's top-level sections (`admin`, `billing`,
|
|
59
|
+
* `manage`, `support`) and every workspace path are what the native
|
|
60
|
+
* deep-link grammar resolves (`DeepLinks.swift`, `DeepLinks.kt`): a
|
|
61
|
+
* registered screen, the Besigner in the app's own web view, or the nearest
|
|
62
|
+
* native screen. `apps/console/specs/native-app-association.spec.ts` holds
|
|
63
|
+
* this list to the console's route set and the native link specs.
|
|
64
|
+
*/ export const NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS = [
|
|
65
|
+
'auth',
|
|
66
|
+
'edit-access',
|
|
67
|
+
'kiosk',
|
|
68
|
+
'signin',
|
|
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',
|
|
81
|
+
'signup',
|
|
82
|
+
'verify-email'
|
|
83
|
+
];
|
|
84
|
+
/**
|
|
85
|
+
* The console paths the apps open, as `applinks` components. Apple reads
|
|
86
|
+
* them in order and the first match wins, so every exclusion comes ahead of
|
|
87
|
+
* the closing catch-all.
|
|
88
|
+
*/ export function consoleAppLinksComponents() {
|
|
89
|
+
const excluded = [
|
|
90
|
+
{
|
|
91
|
+
'/': '/_*',
|
|
92
|
+
exclude: true,
|
|
93
|
+
comment: 'framework and internal paths'
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
'/': '/api',
|
|
97
|
+
exclude: true
|
|
98
|
+
},
|
|
99
|
+
{
|
|
100
|
+
'/': '/api/*',
|
|
101
|
+
exclude: true,
|
|
102
|
+
comment: 'route handlers'
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
'/': '/.well-known/*',
|
|
106
|
+
exclude: true
|
|
107
|
+
}
|
|
108
|
+
];
|
|
109
|
+
for (const segment of NATIVE_WEB_ONLY_TOP_LEVEL_SEGMENTS){
|
|
110
|
+
excluded.push({
|
|
111
|
+
'/': `/${segment}`,
|
|
112
|
+
exclude: true
|
|
113
|
+
}, {
|
|
114
|
+
'/': `/${segment}/*`,
|
|
115
|
+
exclude: true
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
for (const segment of NATIVE_AUTH_LINK_SEGMENTS){
|
|
119
|
+
excluded.push({
|
|
120
|
+
'/': `/${segment}/*`,
|
|
121
|
+
exclude: true
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return [
|
|
125
|
+
...excluded,
|
|
126
|
+
{
|
|
127
|
+
'/': '/*',
|
|
128
|
+
comment: 'workspace, site, account, billing, support and staff pages'
|
|
129
|
+
}
|
|
130
|
+
];
|
|
131
|
+
}
|
|
132
|
+
/** `TEAMID1234.com.example.app`: a 10-character team id, then a bundle id. */ const APPLE_APP_ID = /^[A-Z0-9]{10}\.[A-Za-z0-9-]+(\.[A-Za-z0-9-]+)+$/;
|
|
133
|
+
/** A Java package name: dot-separated identifiers, at least two. */ const ANDROID_PACKAGE = /^[A-Za-z][A-Za-z0-9_]*(\.[A-Za-z][A-Za-z0-9_]*)+$/;
|
|
134
|
+
/** A SHA-256 certificate fingerprint as 32 colon-separated hex pairs. */ const SHA256_FINGERPRINT = /^([0-9A-F]{2}:){31}[0-9A-F]{2}$/;
|
|
135
|
+
/** The Apple app ids in `NATIVE_APPLE_APP_IDS`, in order; invalid ones dropped. */ export function parseAppleAppIds(raw) {
|
|
136
|
+
const ids = (raw != null ? raw : '').split(',').map((value)=>value.trim()).filter((value)=>APPLE_APP_ID.test(value));
|
|
137
|
+
return [
|
|
138
|
+
...new Set(ids)
|
|
139
|
+
];
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* The Android apps in `NATIVE_ANDROID_APPS`. An entry without a valid package
|
|
143
|
+
* or without one valid fingerprint is dropped whole: a statement with no
|
|
144
|
+
* certificate verifies nothing.
|
|
145
|
+
*/ export function parseAndroidApps(raw) {
|
|
146
|
+
const apps = [];
|
|
147
|
+
for (const entry of (raw != null ? raw : '').split(';')){
|
|
148
|
+
const at = entry.indexOf('=');
|
|
149
|
+
if (at < 0) continue;
|
|
150
|
+
const packageName = entry.slice(0, at).trim();
|
|
151
|
+
if (!ANDROID_PACKAGE.test(packageName)) continue;
|
|
152
|
+
if (apps.some((app)=>app.packageName === packageName)) continue;
|
|
153
|
+
const fingerprints = entry.slice(at + 1).split(',').map((value)=>value.trim().toUpperCase()).filter((value)=>SHA256_FINGERPRINT.test(value));
|
|
154
|
+
if (fingerprints.length === 0) continue;
|
|
155
|
+
apps.push({
|
|
156
|
+
packageName,
|
|
157
|
+
sha256Fingerprints: [
|
|
158
|
+
...new Set(fingerprints)
|
|
159
|
+
]
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return apps;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* The association file for these app ids. `applinks` is only for a host
|
|
166
|
+
* whose paths the native deep-link grammar resolves: the console. A host
|
|
167
|
+
* that serves the passkey relying party alone gets `webcredentials` only.
|
|
168
|
+
*
|
|
169
|
+
* One `details` entry per app, in the configured order, because when two
|
|
170
|
+
* installed apps claim the same path the system takes the first.
|
|
171
|
+
*/ export function appleAppSiteAssociation(appIds, options) {
|
|
172
|
+
const document = {
|
|
173
|
+
webcredentials: {
|
|
174
|
+
apps: [
|
|
175
|
+
...appIds
|
|
176
|
+
]
|
|
177
|
+
}
|
|
178
|
+
};
|
|
179
|
+
if (options.applinks) {
|
|
180
|
+
const components = consoleAppLinksComponents();
|
|
181
|
+
document.applinks = {
|
|
182
|
+
apps: [],
|
|
183
|
+
details: appIds.map((appId)=>({
|
|
184
|
+
appIDs: [
|
|
185
|
+
appId
|
|
186
|
+
],
|
|
187
|
+
components
|
|
188
|
+
}))
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
return document;
|
|
192
|
+
}
|
|
193
|
+
/** Passkeys and app links, for every configured app. */ export const ASSET_LINK_RELATIONS = [
|
|
194
|
+
'delegate_permission/common.handle_all_urls',
|
|
195
|
+
'delegate_permission/common.get_login_creds'
|
|
196
|
+
];
|
|
197
|
+
/** The `assetlinks.json` statement list Google's verifier reads. */ export function assetLinks(apps) {
|
|
198
|
+
return apps.map((app)=>({
|
|
199
|
+
relation: [
|
|
200
|
+
...ASSET_LINK_RELATIONS
|
|
201
|
+
],
|
|
202
|
+
target: {
|
|
203
|
+
namespace: 'android_app',
|
|
204
|
+
package_name: app.packageName,
|
|
205
|
+
sha256_cert_fingerprints: [
|
|
206
|
+
...app.sha256Fingerprints
|
|
207
|
+
]
|
|
208
|
+
}
|
|
209
|
+
}));
|
|
210
|
+
}
|
|
211
|
+
/** The request's hostname: lowercase, no port. */ export function requestHostname(host) {
|
|
212
|
+
return (host != null ? host : '').trim().toLowerCase().replace(/:\d+$/, '');
|
|
213
|
+
}
|
|
214
|
+
/** A URL's hostname, or null when it is not one. */ export function hostnameOfUrl(url) {
|
|
215
|
+
if (!url) return null;
|
|
216
|
+
try {
|
|
217
|
+
return new URL(url).hostname.toLowerCase() || null;
|
|
218
|
+
} catch (unused) {
|
|
219
|
+
return null;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/** Short on purpose: Apple's CDN caches on top, and a fix must land within minutes. */ const CACHE_CONTROL = 'public, max-age=300, s-maxage=300';
|
|
223
|
+
/**
|
|
224
|
+
* Whether this host may answer. A Vercel preview deployment is reachable only
|
|
225
|
+
* at the platform's own generated hostnames, so it answers on any host, which
|
|
226
|
+
* is what lets a preview be checked before it is promoted.
|
|
227
|
+
*/ function servesHost(request) {
|
|
228
|
+
if (request.env['VERCEL_ENV'] === 'preview') return true;
|
|
229
|
+
const hostname = requestHostname(request.host);
|
|
230
|
+
if (!hostname) return false;
|
|
231
|
+
return request.servedHosts.some((served)=>requestHostname(served) === hostname);
|
|
232
|
+
}
|
|
233
|
+
function notFound() {
|
|
234
|
+
return new Response('Not Found', {
|
|
235
|
+
status: 404,
|
|
236
|
+
headers: {
|
|
237
|
+
'content-type': 'text/plain; charset=utf-8',
|
|
238
|
+
'cache-control': CACHE_CONTROL
|
|
239
|
+
}
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
function json(body) {
|
|
243
|
+
return new Response(JSON.stringify(body), {
|
|
244
|
+
status: 200,
|
|
245
|
+
headers: {
|
|
246
|
+
'content-type': 'application/json',
|
|
247
|
+
'cache-control': CACHE_CONTROL
|
|
248
|
+
}
|
|
249
|
+
});
|
|
250
|
+
}
|
|
251
|
+
/** `GET /.well-known/apple-app-site-association`. */ export function appleAppSiteAssociationResponse(request) {
|
|
252
|
+
const appIds = parseAppleAppIds(request.env['NATIVE_APPLE_APP_IDS']);
|
|
253
|
+
if (appIds.length === 0 || !servesHost(request)) return notFound();
|
|
254
|
+
return json(appleAppSiteAssociation(appIds, {
|
|
255
|
+
applinks: request.applinks
|
|
256
|
+
}));
|
|
257
|
+
}
|
|
258
|
+
/** `GET /.well-known/assetlinks.json`. */ export function assetLinksResponse(request) {
|
|
259
|
+
const apps = parseAndroidApps(request.env['NATIVE_ANDROID_APPS']);
|
|
260
|
+
if (apps.length === 0 || !servesHost(request)) return notFound();
|
|
261
|
+
return json(assetLinks(apps));
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
//# sourceMappingURL=native-app-association.js.map
|
|
@@ -0,0 +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: 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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"}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* WHAT ENFORCING SINGLE SIGN-ON WOULD DO TO EACH ACCOUNT (AGL-1888, AGL-3671) —
|
|
19
|
+
* the pure half of the Single sign-on card's rehearsal table, with no import at
|
|
20
|
+
* all, so the native apps replay it (`ssoEnforcementAccounts`) beside the
|
|
21
|
+
* console's card, which draws the very same rows.
|
|
22
|
+
*
|
|
23
|
+
* The rehearsal answers one entry per account of the identity pool: the sign-in
|
|
24
|
+
* methods enforcement would remove (`unlinked`), the ones it leaves (`kept`),
|
|
25
|
+
* and why it would leave the account alone (`skipped`). An account can be
|
|
26
|
+
* designated break-glass only if it holds a way in that is not the identity
|
|
27
|
+
* provider itself: anything else would look like protection and provide none.
|
|
28
|
+
*/
|
|
29
|
+
/** One account of the rehearsal, as the single sign-on route answers it. */
|
|
30
|
+
export interface SsoEnforcementAccount {
|
|
31
|
+
uid: string;
|
|
32
|
+
email: string | null;
|
|
33
|
+
unlinked: readonly string[];
|
|
34
|
+
kept: readonly string[];
|
|
35
|
+
skipped?: string;
|
|
36
|
+
}
|
|
37
|
+
/** One row of the rehearsal table, in the words the console shows. */
|
|
38
|
+
export interface SsoEnforcementRow {
|
|
39
|
+
uid: string;
|
|
40
|
+
/** The address, else the account's uid. */
|
|
41
|
+
label: string;
|
|
42
|
+
/** What the account would lose, or why it would lose nothing. */
|
|
43
|
+
wouldLose: string;
|
|
44
|
+
/** What it keeps. */
|
|
45
|
+
keeps: string;
|
|
46
|
+
/** Whether it can be designated break-glass (it holds a way in other than the provider). */
|
|
47
|
+
eligible: boolean;
|
|
48
|
+
/** Whether it is designated break-glass now. */
|
|
49
|
+
designated: boolean;
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Would designating this account actually keep a way in?
|
|
53
|
+
*
|
|
54
|
+
* Mirrors `assessSsoLockoutRisk` deliberately, and over the same fact it
|
|
55
|
+
* uses: an account protects the org only when it holds a provider that is
|
|
56
|
+
* NOT the org's IdP. Designating an account whose sole credential is the SAML
|
|
57
|
+
* link is the most natural way to get this wrong, because it looks exactly
|
|
58
|
+
* like protection and fails in precisely the situation it exists for.
|
|
59
|
+
*
|
|
60
|
+
* The plan is read BEFORE designation, so both halves count: `unlinked` is
|
|
61
|
+
* what enforcement would strip (all non-IdP by construction), and `kept` is
|
|
62
|
+
* what it would leave — which for an already-designated or would-orphan
|
|
63
|
+
* account is the whole set.
|
|
64
|
+
*/
|
|
65
|
+
export declare function protectsIfDesignated(account: Pick<SsoEnforcementAccount, 'unlinked' | 'kept'>, providerId: string | undefined): boolean;
|
|
66
|
+
/** What the account would lose, as the rehearsal table words it. */
|
|
67
|
+
export declare function ssoEnforcementLoses(account: Pick<SsoEnforcementAccount, 'unlinked' | 'skipped'>): string;
|
|
68
|
+
/** The rehearsal table's rows, one per account, for the break-glass choice. */
|
|
69
|
+
export declare function ssoEnforcementAccounts(accounts: readonly SsoEnforcementAccount[] | null | undefined, providerId: string | undefined, designated: readonly string[] | null | undefined): SsoEnforcementRow[];
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/ /**
|
|
17
|
+
* WHAT ENFORCING SINGLE SIGN-ON WOULD DO TO EACH ACCOUNT (AGL-1888, AGL-3671) —
|
|
18
|
+
* the pure half of the Single sign-on card's rehearsal table, with no import at
|
|
19
|
+
* all, so the native apps replay it (`ssoEnforcementAccounts`) beside the
|
|
20
|
+
* console's card, which draws the very same rows.
|
|
21
|
+
*
|
|
22
|
+
* The rehearsal answers one entry per account of the identity pool: the sign-in
|
|
23
|
+
* methods enforcement would remove (`unlinked`), the ones it leaves (`kept`),
|
|
24
|
+
* and why it would leave the account alone (`skipped`). An account can be
|
|
25
|
+
* designated break-glass only if it holds a way in that is not the identity
|
|
26
|
+
* provider itself: anything else would look like protection and provide none.
|
|
27
|
+
*/ /** One account of the rehearsal, as the single sign-on route answers it. */ /**
|
|
28
|
+
* Would designating this account actually keep a way in?
|
|
29
|
+
*
|
|
30
|
+
* Mirrors `assessSsoLockoutRisk` deliberately, and over the same fact it
|
|
31
|
+
* uses: an account protects the org only when it holds a provider that is
|
|
32
|
+
* NOT the org's IdP. Designating an account whose sole credential is the SAML
|
|
33
|
+
* link is the most natural way to get this wrong, because it looks exactly
|
|
34
|
+
* like protection and fails in precisely the situation it exists for.
|
|
35
|
+
*
|
|
36
|
+
* The plan is read BEFORE designation, so both halves count: `unlinked` is
|
|
37
|
+
* what enforcement would strip (all non-IdP by construction), and `kept` is
|
|
38
|
+
* what it would leave — which for an already-designated or would-orphan
|
|
39
|
+
* account is the whole set.
|
|
40
|
+
*/ export function protectsIfDesignated(account, providerId) {
|
|
41
|
+
return [
|
|
42
|
+
...account.unlinked,
|
|
43
|
+
...account.kept
|
|
44
|
+
].some((id)=>id !== providerId);
|
|
45
|
+
}
|
|
46
|
+
/** What the account would lose, as the rehearsal table words it. */ export function ssoEnforcementLoses(account) {
|
|
47
|
+
if (account.skipped === 'break-glass') return 'Nothing — designated break-glass';
|
|
48
|
+
if (account.skipped) return 'Nothing — it would be left with no way in';
|
|
49
|
+
return account.unlinked.join(', ') || '—';
|
|
50
|
+
}
|
|
51
|
+
/** The rehearsal table's rows, one per account, for the break-glass choice. */ export function ssoEnforcementAccounts(accounts, providerId, designated) {
|
|
52
|
+
const chosen = new Set(designated != null ? designated : []);
|
|
53
|
+
return (accounts != null ? accounts : []).map((account)=>{
|
|
54
|
+
var _account_email;
|
|
55
|
+
return {
|
|
56
|
+
uid: account.uid,
|
|
57
|
+
label: (_account_email = account.email) != null ? _account_email : account.uid,
|
|
58
|
+
wouldLose: ssoEnforcementLoses(account),
|
|
59
|
+
keeps: account.kept.join(', ') || '—',
|
|
60
|
+
eligible: protectsIfDesignated(account, providerId),
|
|
61
|
+
designated: chosen.has(account.uid)
|
|
62
|
+
};
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
//# sourceMappingURL=sso-enforcement.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/sso-enforcement.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 * WHAT ENFORCING SINGLE SIGN-ON WOULD DO TO EACH ACCOUNT (AGL-1888, AGL-3671) —\n * the pure half of the Single sign-on card's rehearsal table, with no import at\n * all, so the native apps replay it (`ssoEnforcementAccounts`) beside the\n * console's card, which draws the very same rows.\n *\n * The rehearsal answers one entry per account of the identity pool: the sign-in\n * methods enforcement would remove (`unlinked`), the ones it leaves (`kept`),\n * and why it would leave the account alone (`skipped`). An account can be\n * designated break-glass only if it holds a way in that is not the identity\n * provider itself: anything else would look like protection and provide none.\n */\n\n/** One account of the rehearsal, as the single sign-on route answers it. */\nexport interface SsoEnforcementAccount {\n uid: string\n email: string | null\n unlinked: readonly string[]\n kept: readonly string[]\n skipped?: string\n}\n\n/** One row of the rehearsal table, in the words the console shows. */\nexport interface SsoEnforcementRow {\n uid: string\n /** The address, else the account's uid. */\n label: string\n /** What the account would lose, or why it would lose nothing. */\n wouldLose: string\n /** What it keeps. */\n keeps: string\n /** Whether it can be designated break-glass (it holds a way in other than the provider). */\n eligible: boolean\n /** Whether it is designated break-glass now. */\n designated: boolean\n}\n\n/**\n * Would designating this account actually keep a way in?\n *\n * Mirrors `assessSsoLockoutRisk` deliberately, and over the same fact it\n * uses: an account protects the org only when it holds a provider that is\n * NOT the org's IdP. Designating an account whose sole credential is the SAML\n * link is the most natural way to get this wrong, because it looks exactly\n * like protection and fails in precisely the situation it exists for.\n *\n * The plan is read BEFORE designation, so both halves count: `unlinked` is\n * what enforcement would strip (all non-IdP by construction), and `kept` is\n * what it would leave — which for an already-designated or would-orphan\n * account is the whole set.\n */\nexport function protectsIfDesignated(account: Pick<SsoEnforcementAccount, 'unlinked' | 'kept'>, providerId: string | undefined): boolean {\n return [...account.unlinked, ...account.kept].some((id) => id !== providerId)\n}\n\n/** What the account would lose, as the rehearsal table words it. */\nexport function ssoEnforcementLoses(account: Pick<SsoEnforcementAccount, 'unlinked' | 'skipped'>): string {\n if (account.skipped === 'break-glass') return 'Nothing — designated break-glass'\n if (account.skipped) return 'Nothing — it would be left with no way in'\n return account.unlinked.join(', ') || '—'\n}\n\n/** The rehearsal table's rows, one per account, for the break-glass choice. */\nexport function ssoEnforcementAccounts(\n accounts: readonly SsoEnforcementAccount[] | null | undefined,\n providerId: string | undefined,\n designated: readonly string[] | null | undefined,\n): SsoEnforcementRow[] {\n const chosen = new Set(designated ?? [])\n return (accounts ?? []).map((account) => ({\n uid: account.uid,\n label: account.email ?? account.uid,\n wouldLose: ssoEnforcementLoses(account),\n keeps: account.kept.join(', ') || '—',\n eligible: protectsIfDesignated(account, providerId),\n designated: chosen.has(account.uid),\n }))\n}\n"],"names":["protectsIfDesignated","account","providerId","unlinked","kept","some","id","ssoEnforcementLoses","skipped","join","ssoEnforcementAccounts","accounts","designated","chosen","Set","map","uid","label","email","wouldLose","keeps","eligible","has"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;;;;;;;CAWC,GAED,0EAA0E,GAwB1E;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASA,qBAAqBC,OAAyD,EAAEC,UAA8B;IAC5H,OAAO;WAAID,QAAQE,QAAQ;WAAKF,QAAQG,IAAI;KAAC,CAACC,IAAI,CAAC,CAACC,KAAOA,OAAOJ;AACpE;AAEA,kEAAkE,GAClE,OAAO,SAASK,oBAAoBN,OAA4D;IAC9F,IAAIA,QAAQO,OAAO,KAAK,eAAe,OAAO;IAC9C,IAAIP,QAAQO,OAAO,EAAE,OAAO;IAC5B,OAAOP,QAAQE,QAAQ,CAACM,IAAI,CAAC,SAAS;AACxC;AAEA,6EAA6E,GAC7E,OAAO,SAASC,uBACdC,QAA6D,EAC7DT,UAA8B,EAC9BU,UAAgD;IAEhD,MAAMC,SAAS,IAAIC,IAAIF,qBAAAA,aAAc,EAAE;IACvC,OAAO,CAACD,mBAAAA,WAAY,EAAE,EAAEI,GAAG,CAAC,CAACd;YAEpBA;eAFiC;YACxCe,KAAKf,QAAQe,GAAG;YAChBC,KAAK,GAAEhB,iBAAAA,QAAQiB,KAAK,YAAbjB,iBAAiBA,QAAQe,GAAG;YACnCG,WAAWZ,oBAAoBN;YAC/BmB,OAAOnB,QAAQG,IAAI,CAACK,IAAI,CAAC,SAAS;YAClCY,UAAUrB,qBAAqBC,SAASC;YACxCU,YAAYC,OAAOS,GAAG,CAACrB,QAAQe,GAAG;QACpC;;AACF"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2026 Aglyn LLC
|
|
4
|
+
*
|
|
5
|
+
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
6
|
+
* you may not use this file except in compliance with the License.
|
|
7
|
+
* You may obtain a copy of the License at
|
|
8
|
+
*
|
|
9
|
+
* http://www.apache.org/licenses/LICENSE-2.0
|
|
10
|
+
*
|
|
11
|
+
* Unless required by applicable law or agreed to in writing, software
|
|
12
|
+
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
13
|
+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
14
|
+
* See the License for the specific language governing permissions and
|
|
15
|
+
* limitations under the License.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* THE TIME ZONES THE PICKER OFFERS (AGL-3252, AGL-3671) — the list the console's
|
|
19
|
+
* time zone controls show, kept as data with no import at all, so the native
|
|
20
|
+
* apps replay it (`timeZoneChoices`) beside the console's pickers.
|
|
21
|
+
*
|
|
22
|
+
* The console reads its list off `Intl.supportedValuesOf('timeZone')`
|
|
23
|
+
* (`supportedTimeZones()`), because the IANA database renames zones and a
|
|
24
|
+
* picker built from the runtime cannot offer a name the runtime refuses. An app
|
|
25
|
+
* has no `Intl` to ask, and its platform's own list is a different one (it
|
|
26
|
+
* carries every legacy alias: `US/Eastern`, `GB-Eire`, `Etc/GMT+5`), so the
|
|
27
|
+
* apps offer THIS snapshot of what that call answers: canonical names only,
|
|
28
|
+
* each of which `isSupportedTimeZone` accepts, which is what the workspace
|
|
29
|
+
* route checks. It is a snapshot: when the IANA database moves, regenerate it
|
|
30
|
+
* from `Intl.supportedValuesOf('timeZone')` (the spec beside this file fails
|
|
31
|
+
* for a zone the runtime no longer accepts, which the route would refuse).
|
|
32
|
+
*/
|
|
33
|
+
export declare const TIME_ZONE_CHOICES: readonly string[];
|
|
34
|
+
/** One entry of a picker: the stored zone name, and what the person reads. */
|
|
35
|
+
export interface TimeZoneChoice {
|
|
36
|
+
value: string;
|
|
37
|
+
label: string;
|
|
38
|
+
}
|
|
39
|
+
/** The label of the first choice: an organization that never set a zone dates everything in UTC, and says so. */
|
|
40
|
+
export declare const TIME_ZONE_DEFAULT_LABEL = "UTC (default)";
|
|
41
|
+
/**
|
|
42
|
+
* The picker's choices, in the order the console shows them: the default (an empty value, which the route stores as
|
|
43
|
+
* no zone at all) first, then every zone by name, with its underscores read as spaces (`America/New_York` is
|
|
44
|
+
* `America/New York`).
|
|
45
|
+
*/
|
|
46
|
+
export declare function timeZoneChoices(): TimeZoneChoice[];
|