@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.
Files changed (33) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/activity-labels.js +2 -0
  3. package/src/lib/app-utils/activity-labels.js.map +1 -1
  4. package/src/lib/app-utils/business-profile-values.d.ts +83 -0
  5. package/src/lib/app-utils/business-profile-values.js +160 -0
  6. package/src/lib/app-utils/business-profile-values.js.map +1 -0
  7. package/src/lib/app-utils/business-profile.d.ts +7 -60
  8. package/src/lib/app-utils/business-profile.js +2 -172
  9. package/src/lib/app-utils/business-profile.js.map +1 -1
  10. package/src/lib/app-utils/docs-help-sections.generated.js +8 -5
  11. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  12. package/src/lib/app-utils/docs-help.generated.d.ts +16 -10
  13. package/src/lib/app-utils/docs-help.generated.js +56 -18
  14. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  15. package/src/lib/app-utils/docs-index.generated.js +278 -5
  16. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  17. package/src/lib/app-utils/lockdown.d.ts +6 -0
  18. package/src/lib/app-utils/lockdown.js +20 -0
  19. package/src/lib/app-utils/lockdown.js.map +1 -1
  20. package/src/lib/app-utils/native-app-association.d.ts +139 -0
  21. package/src/lib/app-utils/native-app-association.js +264 -0
  22. package/src/lib/app-utils/native-app-association.js.map +1 -0
  23. package/src/lib/app-utils/sso-enforcement.d.ts +69 -0
  24. package/src/lib/app-utils/sso-enforcement.js +66 -0
  25. package/src/lib/app-utils/sso-enforcement.js.map +1 -0
  26. package/src/lib/app-utils/time-zone-choices.d.ts +46 -0
  27. package/src/lib/app-utils/time-zone-choices.js +469 -0
  28. package/src/lib/app-utils/time-zone-choices.js.map +1 -0
  29. package/src/lib/data-transfer/site-package-diff.d.ts +108 -0
  30. package/src/lib/data-transfer/site-package-diff.js +268 -0
  31. package/src/lib/data-transfer/site-package-diff.js.map +1 -0
  32. package/src/lib/plugin-manager/first-party-plugins.generated.js +2 -2
  33. 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[];