@aglyn/aglyn 1.0.0-beta.285 → 1.0.0-beta.288

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 (91) hide show
  1. package/package.json +11 -11
  2. package/src/lib/app-utils/account-acquisition.d.ts +14 -0
  3. package/src/lib/app-utils/account-acquisition.js +19 -0
  4. package/src/lib/app-utils/account-acquisition.js.map +1 -1
  5. package/src/lib/app-utils/api-plugins.d.ts +25 -0
  6. package/src/lib/app-utils/api-plugins.js +13 -0
  7. package/src/lib/app-utils/api-plugins.js.map +1 -1
  8. package/src/lib/app-utils/business-profile-values.d.ts +15 -3
  9. package/src/lib/app-utils/business-profile-values.js +19 -3
  10. package/src/lib/app-utils/business-profile-values.js.map +1 -1
  11. package/src/lib/app-utils/business-profile.d.ts +5 -3
  12. package/src/lib/app-utils/business-profile.js +2 -1
  13. package/src/lib/app-utils/business-profile.js.map +1 -1
  14. package/src/lib/app-utils/contacts.d.ts +11 -0
  15. package/src/lib/app-utils/contacts.js +18 -0
  16. package/src/lib/app-utils/contacts.js.map +1 -1
  17. package/src/lib/app-utils/docs-deflection.d.ts +1 -1
  18. package/src/lib/app-utils/docs-deflection.js +17 -0
  19. package/src/lib/app-utils/docs-deflection.js.map +1 -1
  20. package/src/lib/app-utils/docs-help-sections.generated.js +5 -2
  21. package/src/lib/app-utils/docs-help-sections.generated.js.map +1 -1
  22. package/src/lib/app-utils/docs-help.generated.d.ts +9 -9
  23. package/src/lib/app-utils/docs-help.generated.js +20 -0
  24. package/src/lib/app-utils/docs-help.generated.js.map +1 -1
  25. package/src/lib/app-utils/docs-index.generated.js +168 -63
  26. package/src/lib/app-utils/docs-index.generated.js.map +1 -1
  27. package/src/lib/app-utils/docs-retrieval.d.ts +17 -0
  28. package/src/lib/app-utils/docs-retrieval.js +28 -0
  29. package/src/lib/app-utils/docs-retrieval.js.map +1 -1
  30. package/src/lib/app-utils/host-naming.d.ts +2 -1
  31. package/src/lib/app-utils/host-naming.js +65 -2
  32. package/src/lib/app-utils/host-naming.js.map +1 -1
  33. package/src/lib/app-utils/host-permissions.d.ts +159 -3
  34. package/src/lib/app-utils/host-permissions.js +384 -12
  35. package/src/lib/app-utils/host-permissions.js.map +1 -1
  36. package/src/lib/app-utils/org-permission-editor.d.ts +128 -1
  37. package/src/lib/app-utils/org-permission-editor.js +176 -1
  38. package/src/lib/app-utils/org-permission-editor.js.map +1 -1
  39. package/src/lib/app-utils/org-permissions.d.ts +62 -1
  40. package/src/lib/app-utils/org-permissions.js +55 -5
  41. package/src/lib/app-utils/org-permissions.js.map +1 -1
  42. package/src/lib/app-utils/org-role-grants.d.ts +28 -0
  43. package/src/lib/app-utils/org-role-grants.js +180 -2
  44. package/src/lib/app-utils/org-role-grants.js.map +1 -1
  45. package/src/lib/app-utils/org-roles.js +11 -2
  46. package/src/lib/app-utils/org-roles.js.map +1 -1
  47. package/src/lib/app-utils/redirect-query.d.ts +39 -0
  48. package/src/lib/app-utils/redirect-query.js +57 -0
  49. package/src/lib/app-utils/redirect-query.js.map +1 -0
  50. package/src/lib/app-utils/release-flags.js +2 -2
  51. package/src/lib/app-utils/release-flags.js.map +1 -1
  52. package/src/lib/app-utils/site-starter-links.d.ts +42 -0
  53. package/src/lib/app-utils/site-starter-links.js +79 -0
  54. package/src/lib/app-utils/site-starter-links.js.map +1 -0
  55. package/src/lib/app-utils/site-starter-nodes.d.ts +91 -0
  56. package/src/lib/app-utils/site-starter-nodes.js +210 -0
  57. package/src/lib/app-utils/site-starter-nodes.js.map +1 -0
  58. package/src/lib/app-utils/site-starter-theme.d.ts +90 -0
  59. package/src/lib/app-utils/site-starter-theme.js +241 -0
  60. package/src/lib/app-utils/site-starter-theme.js.map +1 -0
  61. package/src/lib/app-utils/webhook-delivery.js +6 -1
  62. package/src/lib/app-utils/webhook-delivery.js.map +1 -1
  63. package/src/lib/foundation/definitions/organization.types.d.ts +12 -2
  64. package/src/lib/foundation/definitions/organization.types.js.map +1 -1
  65. package/src/lib/plugin-manager/first-party-plugins.generated.js +39 -0
  66. package/src/lib/plugin-manager/first-party-plugins.generated.js.map +1 -1
  67. package/src/lib/plugin-manager/plugin-closure-notices.d.ts +88 -0
  68. package/src/lib/plugin-manager/plugin-closure-notices.js +123 -0
  69. package/src/lib/plugin-manager/plugin-closure-notices.js.map +1 -0
  70. package/src/lib/plugin-manager/plugin-contact-capture.d.ts +15 -0
  71. package/src/lib/plugin-manager/plugin-contact-capture.js +24 -1
  72. package/src/lib/plugin-manager/plugin-contact-capture.js.map +1 -1
  73. package/src/lib/plugin-manager/plugin-customer-display-verifier.d.ts +77 -0
  74. package/src/lib/plugin-manager/plugin-customer-display-verifier.js +74 -0
  75. package/src/lib/plugin-manager/plugin-customer-display-verifier.js.map +1 -0
  76. package/src/lib/plugin-manager/plugin-customer-display.d.ts +135 -0
  77. package/src/lib/plugin-manager/plugin-customer-display.js +189 -0
  78. package/src/lib/plugin-manager/plugin-customer-display.js.map +1 -0
  79. package/src/lib/plugin-manager/plugin-entitlements.d.ts +59 -0
  80. package/src/lib/plugin-manager/plugin-entitlements.js +32 -3
  81. package/src/lib/plugin-manager/plugin-entitlements.js.map +1 -1
  82. package/src/lib/plugin-manager/plugin-events.d.ts +45 -0
  83. package/src/lib/plugin-manager/plugin-events.js.map +1 -1
  84. package/src/lib/plugin-manager/plugin-permissions.d.ts +74 -0
  85. package/src/lib/plugin-manager/plugin-permissions.js +91 -1
  86. package/src/lib/plugin-manager/plugin-permissions.js.map +1 -1
  87. package/src/lib/plugin-manager/plugin-site-starters.d.ts +7 -0
  88. package/src/lib/plugin-manager/plugin-site-starters.js.map +1 -1
  89. package/src/lib/plugin-manager/plugin-tax-return-sources.d.ts +87 -4
  90. package/src/lib/plugin-manager/plugin-tax-return-sources.js +155 -35
  91. package/src/lib/plugin-manager/plugin-tax-return-sources.js.map +1 -1
@@ -31,6 +31,23 @@ export declare function docsDocumentFrequency(): {
31
31
  frequency: ReadonlyMap<string, number>;
32
32
  sections: number;
33
33
  };
34
+ /**
35
+ * Whether a question has to be put into English before the docs can be
36
+ * searched for it (AGL-4075).
37
+ *
38
+ * The docs are written in English and the search matches words, so a
39
+ * question in another language matches nothing by its own words: 17 of 50
40
+ * chat turns in one fortnight were not in English, and none of them was
41
+ * grounded in the docs at all. True for a question with a letter outside the
42
+ * Latin script; for a Latin-script question fewer than half of whose words
43
+ * appear anywhere in the docs ("¿Cómo conecto mi dominio?"); and for one
44
+ * with a word the docs never use and none of English's small words ("how",
45
+ * "do", "my", "the"), which is how a question in French or Portuguese reads
46
+ * when it shares half its words with English ("Comment publier mon site ?").
47
+ * An English question about the product uses the docs' own words, so it is
48
+ * searched as it was asked. A question with no words has nothing to look up.
49
+ */
50
+ export declare function docsLookupNeedsEnglish(question: string): boolean;
34
51
  /**
35
52
  * Public docs-site origin for deep links the assistant hands the user.
36
53
  *
@@ -148,6 +148,34 @@ export function docsDocumentFrequency() {
148
148
  sections: DOCS_SECTION_INDEX.length
149
149
  };
150
150
  }
151
+ /** A letter of any script but Latin: Arabic, Hebrew, Cyrillic, CJK, Devanagari… */ const NON_LATIN_LETTER = RegExp("[^\\u0000-\\u0040\\u005B-\\u0060\\u007B-\\u00A9\\u00AB-\\u00B4\\u00B6-\\u00B9\\u00BB-\\u00BF\\u00D7\\u00F7\\u02C2-\\u02C5\\u02D2-\\u02DF\\u02E5-\\u02EB\\u02ED\\u02EF-\\u036F\\u0375\\u0378-\\u0379\\u037E\\u0380-\\u0385\\u0387\\u038B\\u038D\\u03A2\\u03F6\\u0482-\\u0489\\u0530\\u0557-\\u0558\\u055A-\\u055F\\u0589-\\u05CF\\u05EB-\\u05EE\\u05F3-\\u061F\\u064B-\\u066D\\u0670\\u06D4\\u06D6-\\u06E4\\u06E7-\\u06ED\\u06F0-\\u06F9\\u06FD-\\u06FE\\u0700-\\u070F\\u0711\\u0730-\\u074C\\u07A6-\\u07B0\\u07B2-\\u07C9\\u07EB-\\u07F3\\u07F6-\\u07F9\\u07FB-\\u07FF\\u0816-\\u0819\\u081B-\\u0823\\u0825-\\u0827\\u0829-\\u083F\\u0859-\\u085F\\u086B-\\u086F\\u0888\\u0890-\\u089F\\u08CA-\\u0903\\u093A-\\u093C\\u093E-\\u094F\\u0951-\\u0957\\u0962-\\u0970\\u0981-\\u0984\\u098D-\\u098E\\u0991-\\u0992\\u09A9\\u09B1\\u09B3-\\u09B5\\u09BA-\\u09BC\\u09BE-\\u09CD\\u09CF-\\u09DB\\u09DE\\u09E2-\\u09EF\\u09F2-\\u09FB\\u09FD-\\u0A04\\u0A0B-\\u0A0E\\u0A11-\\u0A12\\u0A29\\u0A31\\u0A34\\u0A37\\u0A3A-\\u0A58\\u0A5D\\u0A5F-\\u0A71\\u0A75-\\u0A84\\u0A8E\\u0A92\\u0AA9\\u0AB1\\u0AB4\\u0ABA-\\u0ABC\\u0ABE-\\u0ACF\\u0AD1-\\u0ADF\\u0AE2-\\u0AF8\\u0AFA-\\u0B04\\u0B0D-\\u0B0E\\u0B11-\\u0B12\\u0B29\\u0B31\\u0B34\\u0B3A-\\u0B3C\\u0B3E-\\u0B5B\\u0B5E\\u0B62-\\u0B70\\u0B72-\\u0B82\\u0B84\\u0B8B-\\u0B8D\\u0B91\\u0B96-\\u0B98\\u0B9B\\u0B9D\\u0BA0-\\u0BA2\\u0BA5-\\u0BA7\\u0BAB-\\u0BAD\\u0BBA-\\u0BCF\\u0BD1-\\u0C04\\u0C0D\\u0C11\\u0C29\\u0C3A-\\u0C3C\\u0C3E-\\u0C57\\u0C5B\\u0C5E-\\u0C5F\\u0C62-\\u0C7F\\u0C81-\\u0C84\\u0C8D\\u0C91\\u0CA9\\u0CB4\\u0CBA-\\u0CBC\\u0CBE-\\u0CDB\\u0CDF\\u0CE2-\\u0CF0\\u0CF3-\\u0D03\\u0D0D\\u0D11\\u0D3B-\\u0D3C\\u0D3E-\\u0D4D\\u0D4F-\\u0D53\\u0D57-\\u0D5E\\u0D62-\\u0D79\\u0D80-\\u0D84\\u0D97-\\u0D99\\u0DB2\\u0DBC\\u0DBE-\\u0DBF\\u0DC7-\\u0E00\\u0E31\\u0E34-\\u0E3F\\u0E47-\\u0E80\\u0E83\\u0E85\\u0E8B\\u0EA4\\u0EA6\\u0EB1\\u0EB4-\\u0EBC\\u0EBE-\\u0EBF\\u0EC5\\u0EC7-\\u0EDB\\u0EE0-\\u0EFF\\u0F01-\\u0F3F\\u0F48\\u0F6D-\\u0F87\\u0F8D-\\u0FFF\\u102B-\\u103E\\u1040-\\u104F\\u1056-\\u1059\\u105E-\\u1060\\u1062-\\u1064\\u1067-\\u106D\\u1071-\\u1074\\u1082-\\u108D\\u108F-\\u109F\\u10C6\\u10C8-\\u10CC\\u10CE-\\u10CF\\u10FB\\u1249\\u124E-\\u124F\\u1257\\u1259\\u125E-\\u125F\\u1289\\u128E-\\u128F\\u12B1\\u12B6-\\u12B7\\u12BF\\u12C1\\u12C6-\\u12C7\\u12D7\\u1311\\u1316-\\u1317\\u135B-\\u137F\\u1390-\\u139F\\u13F6-\\u13F7\\u13FE-\\u1400\\u166D-\\u166E\\u1680\\u169B-\\u169F\\u16EB-\\u16F0\\u16F9-\\u16FF\\u1712-\\u171E\\u1732-\\u173F\\u1752-\\u175F\\u176D\\u1771-\\u177F\\u17B4-\\u17D6\\u17D8-\\u17DB\\u17DD-\\u181F\\u1879-\\u187F\\u1885-\\u1886\\u18A9\\u18AB-\\u18AF\\u18F6-\\u18FF\\u191F-\\u194F\\u196E-\\u196F\\u1975-\\u197F\\u19AC-\\u19AF\\u19CA-\\u19FF\\u1A17-\\u1A1F\\u1A55-\\u1AA6\\u1AA8-\\u1B04\\u1B34-\\u1B44\\u1B4D-\\u1B82\\u1BA1-\\u1BAD\\u1BB0-\\u1BB9\\u1BE6-\\u1BFF\\u1C24-\\u1C4C\\u1C50-\\u1C59\\u1C7E-\\u1C7F\\u1C8B-\\u1C8F\\u1CBB-\\u1CBC\\u1CC0-\\u1CE8\\u1CED\\u1CF4\\u1CF7-\\u1CF9\\u1CFB-\\u1CFF\\u1DC0-\\u1DFF\\u1F16-\\u1F17\\u1F1E-\\u1F1F\\u1F46-\\u1F47\\u1F4E-\\u1F4F\\u1F58\\u1F5A\\u1F5C\\u1F5E\\u1F7E-\\u1F7F\\u1FB5\\u1FBD\\u1FBF-\\u1FC1\\u1FC5\\u1FCD-\\u1FCF\\u1FD4-\\u1FD5\\u1FDC-\\u1FDF\\u1FED-\\u1FF1\\u1FF5\\u1FFD-\\u2070\\u2072-\\u207E\\u2080-\\u208F\\u209D-\\u2101\\u2103-\\u2106\\u2108-\\u2109\\u2114\\u2116-\\u2118\\u211E-\\u2123\\u2125\\u2127\\u2129\\u212E\\u213A-\\u213B\\u2140-\\u2144\\u214A-\\u214D\\u214F-\\u2182\\u2185-\\u2BFF\\u2CE5-\\u2CEA\\u2CEF-\\u2CF1\\u2CF4-\\u2CFF\\u2D26\\u2D28-\\u2D2C\\u2D2E-\\u2D2F\\u2D68-\\u2D6E\\u2D70-\\u2D7F\\u2D97-\\u2D9F\\u2DA7\\u2DAF\\u2DB7\\u2DBF\\u2DC7\\u2DCF\\u2DD7\\u2DDF-\\u2E2E\\u2E30-\\u3004\\u3007-\\u3030\\u3036-\\u303A\\u303D-\\u3040\\u3097-\\u309C\\u30A0\\u30FB\\u3100-\\u3104\\u3130\\u318F-\\u319F\\u31C0-\\u31EF\\u3200-\\u33FF\\u4DC0-\\u4DFF\\uA48D-\\uA4CF\\uA4FE-\\uA4FF\\uA60D-\\uA60F\\uA620-\\uA629\\uA62C-\\uA63F\\uA66F-\\uA67E\\uA69E-\\uA69F\\uA6E6-\\uA716\\uA720-\\uA721\\uA789-\\uA78A\\uA7DD-\\uA7F0\\uA802\\uA806\\uA80B\\uA823-\\uA83F\\uA874-\\uA881\\uA8B4-\\uA8F1\\uA8F8-\\uA8FA\\uA8FC\\uA8FF-\\uA909\\uA926-\\uA92F\\uA947-\\uA95F\\uA97D-\\uA983\\uA9B3-\\uA9CE\\uA9D0-\\uA9DF\\uA9E5\\uA9F0-\\uA9F9\\uA9FF\\uAA29-\\uAA3F\\uAA43\\uAA4C-\\uAA5F\\uAA77-\\uAA79\\uAA7B-\\uAA7D\\uAAB0\\uAAB2-\\uAAB4\\uAAB7-\\uAAB8\\uAABE-\\uAABF\\uAAC1\\uAAC3-\\uAADA\\uAADE-\\uAADF\\uAAEB-\\uAAF1\\uAAF5-\\uAB00\\uAB07-\\uAB08\\uAB0F-\\uAB10\\uAB17-\\uAB1F\\uAB27\\uAB2F\\uAB5B\\uAB6A-\\uAB6F\\uABE3-\\uABFF\\uD7A4-\\uD7AF\\uD7C7-\\uD7CA\\uD7FC-\\uF8FF\\uFA6E-\\uFA6F\\uFADA-\\uFAFF\\uFB07-\\uFB12\\uFB18-\\uFB1C\\uFB1E\\uFB29\\uFB37\\uFB3D\\uFB3F\\uFB42\\uFB45\\uFBB2-\\uFBD2\\uFD3E-\\uFD4F\\uFD90-\\uFD91\\uFDC8-\\uFDEF\\uFDFC-\\uFE6F\\uFE75\\uFEFD-\\uFF20\\uFF3B-\\uFF40\\uFF5B-\\uFF65\\uFFBF-\\uFFC1\\uFFC8-\\uFFC9\\uFFD0-\\uFFD1\\uFFD8-\\uFFD9\\uFFDD-\\uFFFF\\u{1000C}\\u{10027}\\u{1003B}\\u{1003E}\\u{1004E}-\\u{1004F}\\u{1005E}-\\u{1007F}\\u{100FB}-\\u{1027F}\\u{1029D}-\\u{1029F}\\u{102D1}-\\u{102FF}\\u{10320}-\\u{1032C}\\u{10341}\\u{1034A}-\\u{1034F}\\u{10376}-\\u{1037F}\\u{1039E}-\\u{1039F}\\u{103C4}-\\u{103C7}\\u{103D0}-\\u{103FF}\\u{1049E}-\\u{104AF}\\u{104D4}-\\u{104D7}\\u{104FC}-\\u{104FF}\\u{10528}-\\u{1052F}\\u{10564}-\\u{1056F}\\u{1057B}\\u{1058B}\\u{10593}\\u{10596}\\u{105A2}\\u{105B2}\\u{105BA}\\u{105BD}-\\u{105BF}\\u{105F4}-\\u{105FF}\\u{10737}-\\u{1073F}\\u{10756}-\\u{1075F}\\u{10768}-\\u{1077F}\\u{10786}\\u{107B1}\\u{107BB}-\\u{107FF}\\u{10806}-\\u{10807}\\u{10809}\\u{10836}\\u{10839}-\\u{1083B}\\u{1083D}-\\u{1083E}\\u{10856}-\\u{1085F}\\u{10877}-\\u{1087F}\\u{1089F}-\\u{108DF}\\u{108F3}\\u{108F6}-\\u{108FF}\\u{10916}-\\u{1091F}\\u{1093A}-\\u{1093F}\\u{1095A}-\\u{1097F}\\u{109B8}-\\u{109BD}\\u{109C0}-\\u{109FF}\\u{10A01}-\\u{10A0F}\\u{10A14}\\u{10A18}\\u{10A36}-\\u{10A5F}\\u{10A7D}-\\u{10A7F}\\u{10A9D}-\\u{10ABF}\\u{10AC8}\\u{10AE5}-\\u{10AFF}\\u{10B36}-\\u{10B3F}\\u{10B56}-\\u{10B5F}\\u{10B73}-\\u{10B7F}\\u{10B92}-\\u{10BFF}\\u{10C49}-\\u{10C7F}\\u{10CB3}-\\u{10CBF}\\u{10CF3}-\\u{10CFF}\\u{10D24}-\\u{10D49}\\u{10D66}-\\u{10D6E}\\u{10D86}-\\u{10E7F}\\u{10EAA}-\\u{10EAF}\\u{10EB2}-\\u{10EC1}\\u{10EC8}-\\u{10EFF}\\u{10F1D}-\\u{10F26}\\u{10F28}-\\u{10F2F}\\u{10F46}-\\u{10F6F}\\u{10F82}-\\u{10FAF}\\u{10FC5}-\\u{10FDF}\\u{10FF7}-\\u{11002}\\u{11038}-\\u{11070}\\u{11073}-\\u{11074}\\u{11076}-\\u{11082}\\u{110B0}-\\u{110CF}\\u{110E9}-\\u{11102}\\u{11127}-\\u{11143}\\u{11145}-\\u{11146}\\u{11148}-\\u{1114F}\\u{11173}-\\u{11175}\\u{11177}-\\u{11182}\\u{111B3}-\\u{111C0}\\u{111C5}-\\u{111D9}\\u{111DB}\\u{111DD}-\\u{111FF}\\u{11212}\\u{1122C}-\\u{1123E}\\u{11241}-\\u{1127F}\\u{11287}\\u{11289}\\u{1128E}\\u{1129E}\\u{112A9}-\\u{112AF}\\u{112DF}-\\u{11304}\\u{1130D}-\\u{1130E}\\u{11311}-\\u{11312}\\u{11329}\\u{11331}\\u{11334}\\u{1133A}-\\u{1133C}\\u{1133E}-\\u{1134F}\\u{11351}-\\u{1135C}\\u{11362}-\\u{1137F}\\u{1138A}\\u{1138C}-\\u{1138D}\\u{1138F}\\u{113B6}\\u{113B8}-\\u{113D0}\\u{113D2}\\u{113D4}-\\u{113FF}\\u{11435}-\\u{11446}\\u{1144B}-\\u{1145E}\\u{11462}-\\u{1147F}\\u{114B0}-\\u{114C3}\\u{114C6}\\u{114C8}-\\u{1157F}\\u{115AF}-\\u{115D7}\\u{115DC}-\\u{115FF}\\u{11630}-\\u{11643}\\u{11645}-\\u{1167F}\\u{116AB}-\\u{116B7}\\u{116B9}-\\u{116FF}\\u{1171B}-\\u{1173F}\\u{11747}-\\u{117FF}\\u{1182C}-\\u{1189F}\\u{118E0}-\\u{118FE}\\u{11907}-\\u{11908}\\u{1190A}-\\u{1190B}\\u{11914}\\u{11917}\\u{11930}-\\u{1193E}\\u{11940}\\u{11942}-\\u{1199F}\\u{119A8}-\\u{119A9}\\u{119D1}-\\u{119E0}\\u{119E2}\\u{119E4}-\\u{119FF}\\u{11A01}-\\u{11A0A}\\u{11A33}-\\u{11A39}\\u{11A3B}-\\u{11A4F}\\u{11A51}-\\u{11A5B}\\u{11A8A}-\\u{11A9C}\\u{11A9E}-\\u{11AAF}\\u{11AF9}-\\u{11BBF}\\u{11BE1}-\\u{11BFF}\\u{11C09}\\u{11C2F}-\\u{11C3F}\\u{11C41}-\\u{11C71}\\u{11C90}-\\u{11CFF}\\u{11D07}\\u{11D0A}\\u{11D31}-\\u{11D45}\\u{11D47}-\\u{11D5F}\\u{11D66}\\u{11D69}\\u{11D8A}-\\u{11D97}\\u{11D99}-\\u{11DAF}\\u{11DDC}-\\u{11EDF}\\u{11EF3}-\\u{11F01}\\u{11F03}\\u{11F11}\\u{11F34}-\\u{11FAF}\\u{11FB1}-\\u{11FFF}\\u{1239A}-\\u{1247F}\\u{12544}-\\u{12F8F}\\u{12FF1}-\\u{12FFF}\\u{13430}-\\u{13440}\\u{13447}-\\u{1345F}\\u{143FB}-\\u{143FF}\\u{14647}-\\u{160FF}\\u{1611E}-\\u{167FF}\\u{16A39}-\\u{16A3F}\\u{16A5F}-\\u{16A6F}\\u{16ABF}-\\u{16ACF}\\u{16AEE}-\\u{16AFF}\\u{16B30}-\\u{16B3F}\\u{16B44}-\\u{16B62}\\u{16B78}-\\u{16B7C}\\u{16B90}-\\u{16D3F}\\u{16D6D}-\\u{16E3F}\\u{16E80}-\\u{16E9F}\\u{16EB9}-\\u{16EBA}\\u{16ED4}-\\u{16EFF}\\u{16F4B}-\\u{16F4F}\\u{16F51}-\\u{16F92}\\u{16FA0}-\\u{16FDF}\\u{16FE2}\\u{16FE4}-\\u{16FF1}\\u{16FF4}-\\u{16FFF}\\u{18CD6}-\\u{18CFE}\\u{18D1F}-\\u{18D7F}\\u{18DF3}-\\u{1AFEF}\\u{1AFF4}\\u{1AFFC}\\u{1AFFF}\\u{1B123}-\\u{1B131}\\u{1B133}-\\u{1B14F}\\u{1B153}-\\u{1B154}\\u{1B156}-\\u{1B163}\\u{1B168}-\\u{1B16F}\\u{1B2FC}-\\u{1BBFF}\\u{1BC6B}-\\u{1BC6F}\\u{1BC7D}-\\u{1BC7F}\\u{1BC89}-\\u{1BC8F}\\u{1BC9A}-\\u{1D3FF}\\u{1D455}\\u{1D49D}\\u{1D4A0}-\\u{1D4A1}\\u{1D4A3}-\\u{1D4A4}\\u{1D4A7}-\\u{1D4A8}\\u{1D4AD}\\u{1D4BA}\\u{1D4BC}\\u{1D4C4}\\u{1D506}\\u{1D50B}-\\u{1D50C}\\u{1D515}\\u{1D51D}\\u{1D53A}\\u{1D53F}\\u{1D545}\\u{1D547}-\\u{1D549}\\u{1D551}\\u{1D6A6}-\\u{1D6A7}\\u{1D6C1}\\u{1D6DB}\\u{1D6FB}\\u{1D715}\\u{1D735}\\u{1D74F}\\u{1D76F}\\u{1D789}\\u{1D7A9}\\u{1D7C3}\\u{1D7CC}-\\u{1DEFF}\\u{1DF1F}-\\u{1DF24}\\u{1DF2B}-\\u{1E02F}\\u{1E06E}-\\u{1E0FF}\\u{1E12D}-\\u{1E136}\\u{1E13E}-\\u{1E14D}\\u{1E14F}-\\u{1E28F}\\u{1E2AE}-\\u{1E2BF}\\u{1E2EC}-\\u{1E4CF}\\u{1E4EC}-\\u{1E5CF}\\u{1E5EE}-\\u{1E5EF}\\u{1E5F1}-\\u{1E6BF}\\u{1E6DF}\\u{1E6E3}\\u{1E6E6}\\u{1E6EE}-\\u{1E6EF}\\u{1E6F5}-\\u{1E6FD}\\u{1E700}-\\u{1E7DF}\\u{1E7E7}\\u{1E7EC}\\u{1E7EF}\\u{1E7FF}\\u{1E8C5}-\\u{1E8FF}\\u{1E944}-\\u{1E94A}\\u{1E94C}-\\u{1EDFF}\\u{1EE04}\\u{1EE20}\\u{1EE23}\\u{1EE25}-\\u{1EE26}\\u{1EE28}\\u{1EE33}\\u{1EE38}\\u{1EE3A}\\u{1EE3C}-\\u{1EE41}\\u{1EE43}-\\u{1EE46}\\u{1EE48}\\u{1EE4A}\\u{1EE4C}\\u{1EE50}\\u{1EE53}\\u{1EE55}-\\u{1EE56}\\u{1EE58}\\u{1EE5A}\\u{1EE5C}\\u{1EE5E}\\u{1EE60}\\u{1EE63}\\u{1EE65}-\\u{1EE66}\\u{1EE6B}\\u{1EE73}\\u{1EE78}\\u{1EE7D}\\u{1EE7F}\\u{1EE8A}\\u{1EE9C}-\\u{1EEA0}\\u{1EEA4}\\u{1EEAA}\\u{1EEBC}-\\u{1FFFF}\\u{2A6E0}-\\u{2A6FF}\\u{2B81E}-\\u{2B81F}\\u{2CEAE}-\\u{2CEAF}\\u{2EBE1}-\\u{2EBEF}\\u{2EE5E}-\\u{2F7FF}\\u{2FA1E}-\\u{2FFFF}\\u{3134B}-\\u{3134F}\\u{3347A}-\\u{10FFFF}\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02B8\\u02E0-\\u02E4\\u1D00-\\u1D25\\u1D2C-\\u1D5C\\u1D62-\\u1D65\\u1D6B-\\u1D77\\u1D79-\\u1DBE\\u1E00-\\u1EFF\\u2071\\u207F\\u2090-\\u209C\\u212A-\\u212B\\u2132\\u214E\\u2160-\\u2188\\u2C60-\\u2C7F\\uA722-\\uA787\\uA78B-\\uA7DC\\uA7F1-\\uA7FF\\uAB30-\\uAB5A\\uAB5C-\\uAB64\\uAB66-\\uAB69\\uFB00-\\uFB06\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\u{10780}-\\u{10785}\\u{10787}-\\u{107B0}\\u{107B2}-\\u{107BA}\\u{1DF00}-\\u{1DF1E}\\u{1DF25}-\\u{1DF2A}]", "u");
152
+ /**
153
+ * Whether a question has to be put into English before the docs can be
154
+ * searched for it (AGL-4075).
155
+ *
156
+ * The docs are written in English and the search matches words, so a
157
+ * question in another language matches nothing by its own words: 17 of 50
158
+ * chat turns in one fortnight were not in English, and none of them was
159
+ * grounded in the docs at all. True for a question with a letter outside the
160
+ * Latin script; for a Latin-script question fewer than half of whose words
161
+ * appear anywhere in the docs ("¿Cómo conecto mi dominio?"); and for one
162
+ * with a word the docs never use and none of English's small words ("how",
163
+ * "do", "my", "the"), which is how a question in French or Portuguese reads
164
+ * when it shares half its words with English ("Comment publier mon site ?").
165
+ * An English question about the product uses the docs' own words, so it is
166
+ * searched as it was asked. A question with no words has nothing to look up.
167
+ */ export function docsLookupNeedsEnglish(question) {
168
+ var _text_toLowerCase_match;
169
+ const text = String(question != null ? question : '');
170
+ if (NON_LATIN_LETTER.test(text)) return true;
171
+ const tokens = tokenize(text);
172
+ if (!tokens.length) return false;
173
+ const { frequency } = docsDocumentFrequency();
174
+ const unknown = tokens.filter((token)=>!frequency.has(token)).length;
175
+ if (unknown * 2 > tokens.length) return true;
176
+ const words = (_text_toLowerCase_match = text.toLowerCase().match(RegExp("[\\u0041-\\u005A\\u0061-\\u007A\\u00AA\\u00B5\\u00BA\\u00C0-\\u00D6\\u00D8-\\u00F6\\u00F8-\\u02C1\\u02C6-\\u02D1\\u02E0-\\u02E4\\u02EC\\u02EE\\u0370-\\u0374\\u0376-\\u0377\\u037A-\\u037D\\u037F\\u0386\\u0388-\\u038A\\u038C\\u038E-\\u03A1\\u03A3-\\u03F5\\u03F7-\\u0481\\u048A-\\u052F\\u0531-\\u0556\\u0559\\u0560-\\u0588\\u05D0-\\u05EA\\u05EF-\\u05F2\\u0620-\\u064A\\u066E-\\u066F\\u0671-\\u06D3\\u06D5\\u06E5-\\u06E6\\u06EE-\\u06EF\\u06FA-\\u06FC\\u06FF\\u0710\\u0712-\\u072F\\u074D-\\u07A5\\u07B1\\u07CA-\\u07EA\\u07F4-\\u07F5\\u07FA\\u0800-\\u0815\\u081A\\u0824\\u0828\\u0840-\\u0858\\u0860-\\u086A\\u0870-\\u0887\\u0889-\\u088F\\u08A0-\\u08C9\\u0904-\\u0939\\u093D\\u0950\\u0958-\\u0961\\u0971-\\u0980\\u0985-\\u098C\\u098F-\\u0990\\u0993-\\u09A8\\u09AA-\\u09B0\\u09B2\\u09B6-\\u09B9\\u09BD\\u09CE\\u09DC-\\u09DD\\u09DF-\\u09E1\\u09F0-\\u09F1\\u09FC\\u0A05-\\u0A0A\\u0A0F-\\u0A10\\u0A13-\\u0A28\\u0A2A-\\u0A30\\u0A32-\\u0A33\\u0A35-\\u0A36\\u0A38-\\u0A39\\u0A59-\\u0A5C\\u0A5E\\u0A72-\\u0A74\\u0A85-\\u0A8D\\u0A8F-\\u0A91\\u0A93-\\u0AA8\\u0AAA-\\u0AB0\\u0AB2-\\u0AB3\\u0AB5-\\u0AB9\\u0ABD\\u0AD0\\u0AE0-\\u0AE1\\u0AF9\\u0B05-\\u0B0C\\u0B0F-\\u0B10\\u0B13-\\u0B28\\u0B2A-\\u0B30\\u0B32-\\u0B33\\u0B35-\\u0B39\\u0B3D\\u0B5C-\\u0B5D\\u0B5F-\\u0B61\\u0B71\\u0B83\\u0B85-\\u0B8A\\u0B8E-\\u0B90\\u0B92-\\u0B95\\u0B99-\\u0B9A\\u0B9C\\u0B9E-\\u0B9F\\u0BA3-\\u0BA4\\u0BA8-\\u0BAA\\u0BAE-\\u0BB9\\u0BD0\\u0C05-\\u0C0C\\u0C0E-\\u0C10\\u0C12-\\u0C28\\u0C2A-\\u0C39\\u0C3D\\u0C58-\\u0C5A\\u0C5C-\\u0C5D\\u0C60-\\u0C61\\u0C80\\u0C85-\\u0C8C\\u0C8E-\\u0C90\\u0C92-\\u0CA8\\u0CAA-\\u0CB3\\u0CB5-\\u0CB9\\u0CBD\\u0CDC-\\u0CDE\\u0CE0-\\u0CE1\\u0CF1-\\u0CF2\\u0D04-\\u0D0C\\u0D0E-\\u0D10\\u0D12-\\u0D3A\\u0D3D\\u0D4E\\u0D54-\\u0D56\\u0D5F-\\u0D61\\u0D7A-\\u0D7F\\u0D85-\\u0D96\\u0D9A-\\u0DB1\\u0DB3-\\u0DBB\\u0DBD\\u0DC0-\\u0DC6\\u0E01-\\u0E30\\u0E32-\\u0E33\\u0E40-\\u0E46\\u0E81-\\u0E82\\u0E84\\u0E86-\\u0E8A\\u0E8C-\\u0EA3\\u0EA5\\u0EA7-\\u0EB0\\u0EB2-\\u0EB3\\u0EBD\\u0EC0-\\u0EC4\\u0EC6\\u0EDC-\\u0EDF\\u0F00\\u0F40-\\u0F47\\u0F49-\\u0F6C\\u0F88-\\u0F8C\\u1000-\\u102A\\u103F\\u1050-\\u1055\\u105A-\\u105D\\u1061\\u1065-\\u1066\\u106E-\\u1070\\u1075-\\u1081\\u108E\\u10A0-\\u10C5\\u10C7\\u10CD\\u10D0-\\u10FA\\u10FC-\\u1248\\u124A-\\u124D\\u1250-\\u1256\\u1258\\u125A-\\u125D\\u1260-\\u1288\\u128A-\\u128D\\u1290-\\u12B0\\u12B2-\\u12B5\\u12B8-\\u12BE\\u12C0\\u12C2-\\u12C5\\u12C8-\\u12D6\\u12D8-\\u1310\\u1312-\\u1315\\u1318-\\u135A\\u1380-\\u138F\\u13A0-\\u13F5\\u13F8-\\u13FD\\u1401-\\u166C\\u166F-\\u167F\\u1681-\\u169A\\u16A0-\\u16EA\\u16F1-\\u16F8\\u1700-\\u1711\\u171F-\\u1731\\u1740-\\u1751\\u1760-\\u176C\\u176E-\\u1770\\u1780-\\u17B3\\u17D7\\u17DC\\u1820-\\u1878\\u1880-\\u1884\\u1887-\\u18A8\\u18AA\\u18B0-\\u18F5\\u1900-\\u191E\\u1950-\\u196D\\u1970-\\u1974\\u1980-\\u19AB\\u19B0-\\u19C9\\u1A00-\\u1A16\\u1A20-\\u1A54\\u1AA7\\u1B05-\\u1B33\\u1B45-\\u1B4C\\u1B83-\\u1BA0\\u1BAE-\\u1BAF\\u1BBA-\\u1BE5\\u1C00-\\u1C23\\u1C4D-\\u1C4F\\u1C5A-\\u1C7D\\u1C80-\\u1C8A\\u1C90-\\u1CBA\\u1CBD-\\u1CBF\\u1CE9-\\u1CEC\\u1CEE-\\u1CF3\\u1CF5-\\u1CF6\\u1CFA\\u1D00-\\u1DBF\\u1E00-\\u1F15\\u1F18-\\u1F1D\\u1F20-\\u1F45\\u1F48-\\u1F4D\\u1F50-\\u1F57\\u1F59\\u1F5B\\u1F5D\\u1F5F-\\u1F7D\\u1F80-\\u1FB4\\u1FB6-\\u1FBC\\u1FBE\\u1FC2-\\u1FC4\\u1FC6-\\u1FCC\\u1FD0-\\u1FD3\\u1FD6-\\u1FDB\\u1FE0-\\u1FEC\\u1FF2-\\u1FF4\\u1FF6-\\u1FFC\\u2071\\u207F\\u2090-\\u209C\\u2102\\u2107\\u210A-\\u2113\\u2115\\u2119-\\u211D\\u2124\\u2126\\u2128\\u212A-\\u212D\\u212F-\\u2139\\u213C-\\u213F\\u2145-\\u2149\\u214E\\u2183-\\u2184\\u2C00-\\u2CE4\\u2CEB-\\u2CEE\\u2CF2-\\u2CF3\\u2D00-\\u2D25\\u2D27\\u2D2D\\u2D30-\\u2D67\\u2D6F\\u2D80-\\u2D96\\u2DA0-\\u2DA6\\u2DA8-\\u2DAE\\u2DB0-\\u2DB6\\u2DB8-\\u2DBE\\u2DC0-\\u2DC6\\u2DC8-\\u2DCE\\u2DD0-\\u2DD6\\u2DD8-\\u2DDE\\u2E2F\\u3005-\\u3006\\u3031-\\u3035\\u303B-\\u303C\\u3041-\\u3096\\u309D-\\u309F\\u30A1-\\u30FA\\u30FC-\\u30FF\\u3105-\\u312F\\u3131-\\u318E\\u31A0-\\u31BF\\u31F0-\\u31FF\\u3400-\\u4DBF\\u4E00-\\uA48C\\uA4D0-\\uA4FD\\uA500-\\uA60C\\uA610-\\uA61F\\uA62A-\\uA62B\\uA640-\\uA66E\\uA67F-\\uA69D\\uA6A0-\\uA6E5\\uA717-\\uA71F\\uA722-\\uA788\\uA78B-\\uA7DC\\uA7F1-\\uA801\\uA803-\\uA805\\uA807-\\uA80A\\uA80C-\\uA822\\uA840-\\uA873\\uA882-\\uA8B3\\uA8F2-\\uA8F7\\uA8FB\\uA8FD-\\uA8FE\\uA90A-\\uA925\\uA930-\\uA946\\uA960-\\uA97C\\uA984-\\uA9B2\\uA9CF\\uA9E0-\\uA9E4\\uA9E6-\\uA9EF\\uA9FA-\\uA9FE\\uAA00-\\uAA28\\uAA40-\\uAA42\\uAA44-\\uAA4B\\uAA60-\\uAA76\\uAA7A\\uAA7E-\\uAAAF\\uAAB1\\uAAB5-\\uAAB6\\uAAB9-\\uAABD\\uAAC0\\uAAC2\\uAADB-\\uAADD\\uAAE0-\\uAAEA\\uAAF2-\\uAAF4\\uAB01-\\uAB06\\uAB09-\\uAB0E\\uAB11-\\uAB16\\uAB20-\\uAB26\\uAB28-\\uAB2E\\uAB30-\\uAB5A\\uAB5C-\\uAB69\\uAB70-\\uABE2\\uAC00-\\uD7A3\\uD7B0-\\uD7C6\\uD7CB-\\uD7FB\\uF900-\\uFA6D\\uFA70-\\uFAD9\\uFB00-\\uFB06\\uFB13-\\uFB17\\uFB1D\\uFB1F-\\uFB28\\uFB2A-\\uFB36\\uFB38-\\uFB3C\\uFB3E\\uFB40-\\uFB41\\uFB43-\\uFB44\\uFB46-\\uFBB1\\uFBD3-\\uFD3D\\uFD50-\\uFD8F\\uFD92-\\uFDC7\\uFDF0-\\uFDFB\\uFE70-\\uFE74\\uFE76-\\uFEFC\\uFF21-\\uFF3A\\uFF41-\\uFF5A\\uFF66-\\uFFBE\\uFFC2-\\uFFC7\\uFFCA-\\uFFCF\\uFFD2-\\uFFD7\\uFFDA-\\uFFDC\\u{10000}-\\u{1000B}\\u{1000D}-\\u{10026}\\u{10028}-\\u{1003A}\\u{1003C}-\\u{1003D}\\u{1003F}-\\u{1004D}\\u{10050}-\\u{1005D}\\u{10080}-\\u{100FA}\\u{10280}-\\u{1029C}\\u{102A0}-\\u{102D0}\\u{10300}-\\u{1031F}\\u{1032D}-\\u{10340}\\u{10342}-\\u{10349}\\u{10350}-\\u{10375}\\u{10380}-\\u{1039D}\\u{103A0}-\\u{103C3}\\u{103C8}-\\u{103CF}\\u{10400}-\\u{1049D}\\u{104B0}-\\u{104D3}\\u{104D8}-\\u{104FB}\\u{10500}-\\u{10527}\\u{10530}-\\u{10563}\\u{10570}-\\u{1057A}\\u{1057C}-\\u{1058A}\\u{1058C}-\\u{10592}\\u{10594}-\\u{10595}\\u{10597}-\\u{105A1}\\u{105A3}-\\u{105B1}\\u{105B3}-\\u{105B9}\\u{105BB}-\\u{105BC}\\u{105C0}-\\u{105F3}\\u{10600}-\\u{10736}\\u{10740}-\\u{10755}\\u{10760}-\\u{10767}\\u{10780}-\\u{10785}\\u{10787}-\\u{107B0}\\u{107B2}-\\u{107BA}\\u{10800}-\\u{10805}\\u{10808}\\u{1080A}-\\u{10835}\\u{10837}-\\u{10838}\\u{1083C}\\u{1083F}-\\u{10855}\\u{10860}-\\u{10876}\\u{10880}-\\u{1089E}\\u{108E0}-\\u{108F2}\\u{108F4}-\\u{108F5}\\u{10900}-\\u{10915}\\u{10920}-\\u{10939}\\u{10940}-\\u{10959}\\u{10980}-\\u{109B7}\\u{109BE}-\\u{109BF}\\u{10A00}\\u{10A10}-\\u{10A13}\\u{10A15}-\\u{10A17}\\u{10A19}-\\u{10A35}\\u{10A60}-\\u{10A7C}\\u{10A80}-\\u{10A9C}\\u{10AC0}-\\u{10AC7}\\u{10AC9}-\\u{10AE4}\\u{10B00}-\\u{10B35}\\u{10B40}-\\u{10B55}\\u{10B60}-\\u{10B72}\\u{10B80}-\\u{10B91}\\u{10C00}-\\u{10C48}\\u{10C80}-\\u{10CB2}\\u{10CC0}-\\u{10CF2}\\u{10D00}-\\u{10D23}\\u{10D4A}-\\u{10D65}\\u{10D6F}-\\u{10D85}\\u{10E80}-\\u{10EA9}\\u{10EB0}-\\u{10EB1}\\u{10EC2}-\\u{10EC7}\\u{10F00}-\\u{10F1C}\\u{10F27}\\u{10F30}-\\u{10F45}\\u{10F70}-\\u{10F81}\\u{10FB0}-\\u{10FC4}\\u{10FE0}-\\u{10FF6}\\u{11003}-\\u{11037}\\u{11071}-\\u{11072}\\u{11075}\\u{11083}-\\u{110AF}\\u{110D0}-\\u{110E8}\\u{11103}-\\u{11126}\\u{11144}\\u{11147}\\u{11150}-\\u{11172}\\u{11176}\\u{11183}-\\u{111B2}\\u{111C1}-\\u{111C4}\\u{111DA}\\u{111DC}\\u{11200}-\\u{11211}\\u{11213}-\\u{1122B}\\u{1123F}-\\u{11240}\\u{11280}-\\u{11286}\\u{11288}\\u{1128A}-\\u{1128D}\\u{1128F}-\\u{1129D}\\u{1129F}-\\u{112A8}\\u{112B0}-\\u{112DE}\\u{11305}-\\u{1130C}\\u{1130F}-\\u{11310}\\u{11313}-\\u{11328}\\u{1132A}-\\u{11330}\\u{11332}-\\u{11333}\\u{11335}-\\u{11339}\\u{1133D}\\u{11350}\\u{1135D}-\\u{11361}\\u{11380}-\\u{11389}\\u{1138B}\\u{1138E}\\u{11390}-\\u{113B5}\\u{113B7}\\u{113D1}\\u{113D3}\\u{11400}-\\u{11434}\\u{11447}-\\u{1144A}\\u{1145F}-\\u{11461}\\u{11480}-\\u{114AF}\\u{114C4}-\\u{114C5}\\u{114C7}\\u{11580}-\\u{115AE}\\u{115D8}-\\u{115DB}\\u{11600}-\\u{1162F}\\u{11644}\\u{11680}-\\u{116AA}\\u{116B8}\\u{11700}-\\u{1171A}\\u{11740}-\\u{11746}\\u{11800}-\\u{1182B}\\u{118A0}-\\u{118DF}\\u{118FF}-\\u{11906}\\u{11909}\\u{1190C}-\\u{11913}\\u{11915}-\\u{11916}\\u{11918}-\\u{1192F}\\u{1193F}\\u{11941}\\u{119A0}-\\u{119A7}\\u{119AA}-\\u{119D0}\\u{119E1}\\u{119E3}\\u{11A00}\\u{11A0B}-\\u{11A32}\\u{11A3A}\\u{11A50}\\u{11A5C}-\\u{11A89}\\u{11A9D}\\u{11AB0}-\\u{11AF8}\\u{11BC0}-\\u{11BE0}\\u{11C00}-\\u{11C08}\\u{11C0A}-\\u{11C2E}\\u{11C40}\\u{11C72}-\\u{11C8F}\\u{11D00}-\\u{11D06}\\u{11D08}-\\u{11D09}\\u{11D0B}-\\u{11D30}\\u{11D46}\\u{11D60}-\\u{11D65}\\u{11D67}-\\u{11D68}\\u{11D6A}-\\u{11D89}\\u{11D98}\\u{11DB0}-\\u{11DDB}\\u{11EE0}-\\u{11EF2}\\u{11F02}\\u{11F04}-\\u{11F10}\\u{11F12}-\\u{11F33}\\u{11FB0}\\u{12000}-\\u{12399}\\u{12480}-\\u{12543}\\u{12F90}-\\u{12FF0}\\u{13000}-\\u{1342F}\\u{13441}-\\u{13446}\\u{13460}-\\u{143FA}\\u{14400}-\\u{14646}\\u{16100}-\\u{1611D}\\u{16800}-\\u{16A38}\\u{16A40}-\\u{16A5E}\\u{16A70}-\\u{16ABE}\\u{16AD0}-\\u{16AED}\\u{16B00}-\\u{16B2F}\\u{16B40}-\\u{16B43}\\u{16B63}-\\u{16B77}\\u{16B7D}-\\u{16B8F}\\u{16D40}-\\u{16D6C}\\u{16E40}-\\u{16E7F}\\u{16EA0}-\\u{16EB8}\\u{16EBB}-\\u{16ED3}\\u{16F00}-\\u{16F4A}\\u{16F50}\\u{16F93}-\\u{16F9F}\\u{16FE0}-\\u{16FE1}\\u{16FE3}\\u{16FF2}-\\u{16FF3}\\u{17000}-\\u{18CD5}\\u{18CFF}-\\u{18D1E}\\u{18D80}-\\u{18DF2}\\u{1AFF0}-\\u{1AFF3}\\u{1AFF5}-\\u{1AFFB}\\u{1AFFD}-\\u{1AFFE}\\u{1B000}-\\u{1B122}\\u{1B132}\\u{1B150}-\\u{1B152}\\u{1B155}\\u{1B164}-\\u{1B167}\\u{1B170}-\\u{1B2FB}\\u{1BC00}-\\u{1BC6A}\\u{1BC70}-\\u{1BC7C}\\u{1BC80}-\\u{1BC88}\\u{1BC90}-\\u{1BC99}\\u{1D400}-\\u{1D454}\\u{1D456}-\\u{1D49C}\\u{1D49E}-\\u{1D49F}\\u{1D4A2}\\u{1D4A5}-\\u{1D4A6}\\u{1D4A9}-\\u{1D4AC}\\u{1D4AE}-\\u{1D4B9}\\u{1D4BB}\\u{1D4BD}-\\u{1D4C3}\\u{1D4C5}-\\u{1D505}\\u{1D507}-\\u{1D50A}\\u{1D50D}-\\u{1D514}\\u{1D516}-\\u{1D51C}\\u{1D51E}-\\u{1D539}\\u{1D53B}-\\u{1D53E}\\u{1D540}-\\u{1D544}\\u{1D546}\\u{1D54A}-\\u{1D550}\\u{1D552}-\\u{1D6A5}\\u{1D6A8}-\\u{1D6C0}\\u{1D6C2}-\\u{1D6DA}\\u{1D6DC}-\\u{1D6FA}\\u{1D6FC}-\\u{1D714}\\u{1D716}-\\u{1D734}\\u{1D736}-\\u{1D74E}\\u{1D750}-\\u{1D76E}\\u{1D770}-\\u{1D788}\\u{1D78A}-\\u{1D7A8}\\u{1D7AA}-\\u{1D7C2}\\u{1D7C4}-\\u{1D7CB}\\u{1DF00}-\\u{1DF1E}\\u{1DF25}-\\u{1DF2A}\\u{1E030}-\\u{1E06D}\\u{1E100}-\\u{1E12C}\\u{1E137}-\\u{1E13D}\\u{1E14E}\\u{1E290}-\\u{1E2AD}\\u{1E2C0}-\\u{1E2EB}\\u{1E4D0}-\\u{1E4EB}\\u{1E5D0}-\\u{1E5ED}\\u{1E5F0}\\u{1E6C0}-\\u{1E6DE}\\u{1E6E0}-\\u{1E6E2}\\u{1E6E4}-\\u{1E6E5}\\u{1E6E7}-\\u{1E6ED}\\u{1E6F0}-\\u{1E6F4}\\u{1E6FE}-\\u{1E6FF}\\u{1E7E0}-\\u{1E7E6}\\u{1E7E8}-\\u{1E7EB}\\u{1E7ED}-\\u{1E7EE}\\u{1E7F0}-\\u{1E7FE}\\u{1E800}-\\u{1E8C4}\\u{1E900}-\\u{1E943}\\u{1E94B}\\u{1EE00}-\\u{1EE03}\\u{1EE05}-\\u{1EE1F}\\u{1EE21}-\\u{1EE22}\\u{1EE24}\\u{1EE27}\\u{1EE29}-\\u{1EE32}\\u{1EE34}-\\u{1EE37}\\u{1EE39}\\u{1EE3B}\\u{1EE42}\\u{1EE47}\\u{1EE49}\\u{1EE4B}\\u{1EE4D}-\\u{1EE4F}\\u{1EE51}-\\u{1EE52}\\u{1EE54}\\u{1EE57}\\u{1EE59}\\u{1EE5B}\\u{1EE5D}\\u{1EE5F}\\u{1EE61}-\\u{1EE62}\\u{1EE64}\\u{1EE67}-\\u{1EE6A}\\u{1EE6C}-\\u{1EE72}\\u{1EE74}-\\u{1EE77}\\u{1EE79}-\\u{1EE7C}\\u{1EE7E}\\u{1EE80}-\\u{1EE89}\\u{1EE8B}-\\u{1EE9B}\\u{1EEA1}-\\u{1EEA3}\\u{1EEA5}-\\u{1EEA9}\\u{1EEAB}-\\u{1EEBB}\\u{20000}-\\u{2A6DF}\\u{2A700}-\\u{2B81D}\\u{2B820}-\\u{2CEAD}\\u{2CEB0}-\\u{2EBE0}\\u{2EBF0}-\\u{2EE5D}\\u{2F800}-\\u{2FA1D}\\u{30000}-\\u{3134A}\\u{31350}-\\u{33479}\\u0030-\\u0039\\u00B2-\\u00B3\\u00B9\\u00BC-\\u00BE\\u0660-\\u0669\\u06F0-\\u06F9\\u07C0-\\u07C9\\u0966-\\u096F\\u09E6-\\u09EF\\u09F4-\\u09F9\\u0A66-\\u0A6F\\u0AE6-\\u0AEF\\u0B66-\\u0B6F\\u0B72-\\u0B77\\u0BE6-\\u0BF2\\u0C66-\\u0C6F\\u0C78-\\u0C7E\\u0CE6-\\u0CEF\\u0D58-\\u0D5E\\u0D66-\\u0D78\\u0DE6-\\u0DEF\\u0E50-\\u0E59\\u0ED0-\\u0ED9\\u0F20-\\u0F33\\u1040-\\u1049\\u1090-\\u1099\\u1369-\\u137C\\u16EE-\\u16F0\\u17E0-\\u17E9\\u17F0-\\u17F9\\u1810-\\u1819\\u1946-\\u194F\\u19D0-\\u19DA\\u1A80-\\u1A89\\u1A90-\\u1A99\\u1B50-\\u1B59\\u1BB0-\\u1BB9\\u1C40-\\u1C49\\u1C50-\\u1C59\\u2070\\u2074-\\u2079\\u2080-\\u2089\\u2150-\\u2182\\u2185-\\u2189\\u2460-\\u249B\\u24EA-\\u24FF\\u2776-\\u2793\\u2CFD\\u3007\\u3021-\\u3029\\u3038-\\u303A\\u3192-\\u3195\\u3220-\\u3229\\u3248-\\u324F\\u3251-\\u325F\\u3280-\\u3289\\u32B1-\\u32BF\\uA620-\\uA629\\uA6E6-\\uA6EF\\uA830-\\uA835\\uA8D0-\\uA8D9\\uA900-\\uA909\\uA9D0-\\uA9D9\\uA9F0-\\uA9F9\\uAA50-\\uAA59\\uABF0-\\uABF9\\uFF10-\\uFF19\\u{10107}-\\u{10133}\\u{10140}-\\u{10178}\\u{1018A}-\\u{1018B}\\u{102E1}-\\u{102FB}\\u{10320}-\\u{10323}\\u{10341}\\u{1034A}\\u{103D1}-\\u{103D5}\\u{104A0}-\\u{104A9}\\u{10858}-\\u{1085F}\\u{10879}-\\u{1087F}\\u{108A7}-\\u{108AF}\\u{108FB}-\\u{108FF}\\u{10916}-\\u{1091B}\\u{109BC}-\\u{109BD}\\u{109C0}-\\u{109CF}\\u{109D2}-\\u{109FF}\\u{10A40}-\\u{10A48}\\u{10A7D}-\\u{10A7E}\\u{10A9D}-\\u{10A9F}\\u{10AEB}-\\u{10AEF}\\u{10B58}-\\u{10B5F}\\u{10B78}-\\u{10B7F}\\u{10BA9}-\\u{10BAF}\\u{10CFA}-\\u{10CFF}\\u{10D30}-\\u{10D39}\\u{10D40}-\\u{10D49}\\u{10E60}-\\u{10E7E}\\u{10F1D}-\\u{10F26}\\u{10F51}-\\u{10F54}\\u{10FC5}-\\u{10FCB}\\u{11052}-\\u{1106F}\\u{110F0}-\\u{110F9}\\u{11136}-\\u{1113F}\\u{111D0}-\\u{111D9}\\u{111E1}-\\u{111F4}\\u{112F0}-\\u{112F9}\\u{11450}-\\u{11459}\\u{114D0}-\\u{114D9}\\u{11650}-\\u{11659}\\u{116C0}-\\u{116C9}\\u{116D0}-\\u{116E3}\\u{11730}-\\u{1173B}\\u{118E0}-\\u{118F2}\\u{11950}-\\u{11959}\\u{11BF0}-\\u{11BF9}\\u{11C50}-\\u{11C6C}\\u{11D50}-\\u{11D59}\\u{11DA0}-\\u{11DA9}\\u{11DE0}-\\u{11DE9}\\u{11F50}-\\u{11F59}\\u{11FC0}-\\u{11FD4}\\u{12400}-\\u{1246E}\\u{16130}-\\u{16139}\\u{16A60}-\\u{16A69}\\u{16AC0}-\\u{16AC9}\\u{16B50}-\\u{16B59}\\u{16B5B}-\\u{16B61}\\u{16D70}-\\u{16D79}\\u{16E80}-\\u{16E96}\\u{16FF4}-\\u{16FF6}\\u{1CCF0}-\\u{1CCF9}\\u{1D2C0}-\\u{1D2D3}\\u{1D2E0}-\\u{1D2F3}\\u{1D360}-\\u{1D378}\\u{1D7CE}-\\u{1D7FF}\\u{1E140}-\\u{1E149}\\u{1E2F0}-\\u{1E2F9}\\u{1E4F0}-\\u{1E4F9}\\u{1E5F1}-\\u{1E5FA}\\u{1E8C7}-\\u{1E8CF}\\u{1E950}-\\u{1E959}\\u{1EC71}-\\u{1ECAB}\\u{1ECAD}-\\u{1ECAF}\\u{1ECB1}-\\u{1ECB4}\\u{1ED01}-\\u{1ED2D}\\u{1ED2F}-\\u{1ED3D}\\u{1F100}-\\u{1F10C}\\u{1FBF0}-\\u{1FBF9}]{2,}", "gu"))) != null ? _text_toLowerCase_match : [];
177
+ return unknown > 0 && !words.some((word)=>STOP_WORDS.has(word));
178
+ }
151
179
  /**
152
180
  * Public docs-site origin for deep links the assistant hands the user.
153
181
  *
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/docs-retrieval.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\nimport {\n DOCS_SECTION_INDEX,\n type DocsSection,\n} from './docs-index.generated'\nimport { DOCS_BASE_URL } from './docs-help'\n\n/**\n * Lexical docs search (AGL-1860, phase 1) — deliberately no vector store: a\n * BM25-flavoured term-frequency score over the generated docs section index,\n * with title/heading term boosts. Good enough to find the right docs section\n * for a \"how do I…\" question and cheap enough to run on every request inside\n * an API route.\n *\n * The platform's, and named so (AGL-3080): the console's issue reports\n * deflect to it with no model involved, and the AI plugin grounds its answers\n * in it — one reader among others, not its owner.\n */\n\n/** Words too common in this corpus to carry signal. */\nconst STOP_WORDS = new Set([\n 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'can', 'do', 'does',\n 'for', 'from', 'how', 'i', 'in', 'is', 'it', 'my', 'of', 'on', 'or',\n 'that', 'the', 'this', 'to', 'what', 'when', 'where', 'which', 'why',\n 'with', 'you', 'your',\n])\n\n/** Lowercase word tokens, stop words removed, length >= 2. */\nexport function tokenize(text: string): string[] {\n return (text.toLowerCase().match(/[\\p{L}\\p{N}]{2,}/gu) ?? []).filter(\n (token) => !STOP_WORDS.has(token),\n )\n}\n\nexport interface ScoredSection {\n section: DocsSection\n score: number\n}\n\n/**\n * Score one section against query tokens. Term frequency saturates quickly\n * (BM25-ish k=1.2) so one section mentioning \"domain\" ten times does not\n * drown a section about the actual task; title and heading hits carry\n * dedicated boosts because those are the docs' own statement of topic.\n */\nfunction scoreSection(\n section: DocsSection,\n queryTokens: readonly string[],\n bodyTokenCounts: Map<string, number>,\n titleTokens: Set<string>,\n headingTokens: Set<string>,\n): number {\n let score = 0\n for (const token of queryTokens) {\n const tf = bodyTokenCounts.get(token) ?? 0\n if (tf > 0) score += (tf * 2.2) / (tf + 1.2)\n if (titleTokens.has(token)) score += 2.5\n if (headingTokens.has(token)) score += 2\n }\n return score\n}\n\ninterface IndexedSection {\n section: DocsSection\n bodyTokenCounts: Map<string, number>\n titleTokens: Set<string>\n headingTokens: Set<string>\n}\n\n/** Tokenized once per process — module scope, ~650 sections. */\nlet indexed: IndexedSection[] | null = null\n\nfunction buildIndex(sections: readonly DocsSection[]): IndexedSection[] {\n return sections.map((section) => {\n const bodyTokenCounts = new Map<string, number>()\n for (const token of tokenize(section.text)) {\n bodyTokenCounts.set(token, (bodyTokenCounts.get(token) ?? 0) + 1)\n }\n return {\n section,\n bodyTokenCounts,\n titleTokens: new Set(tokenize(section.title)),\n headingTokens: new Set(tokenize(section.heading)),\n }\n })\n}\n\n/**\n * Top-`limit` docs sections for a user question. Returns an empty array when\n * nothing scores above zero — the caller then answers without docs grounding\n * rather than citing an irrelevant page.\n */\nexport function retrieveDocsSections(\n question: string,\n limit = 6,\n sections: readonly DocsSection[] = DOCS_SECTION_INDEX,\n): ScoredSection[] {\n const queryTokens = [...new Set(tokenize(question))]\n if (!queryTokens.length) return []\n const index =\n sections === DOCS_SECTION_INDEX\n ? (indexed ??= buildIndex(sections))\n : buildIndex(sections)\n return index\n .map((entry) => ({\n section: entry.section,\n score: scoreSection(\n entry.section,\n queryTokens,\n entry.bodyTokenCounts,\n entry.titleTokens,\n entry.headingTokens,\n ),\n }))\n .filter((scored) => scored.score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, limit)\n}\n\n/**\n * How many indexed sections each token appears in — the raw material for an\n * IDF weight (AGL-2486).\n *\n * Lives here rather than in the deflection module because the tokenized index\n * is already built here and rebuilding it there would tokenize ~500KB of docs\n * a second time, per process, to reach the same numbers. Memoised alongside\n * `indexed` and derived from it, so the two can never disagree about what a\n * token is.\n *\n * Counts SECTIONS, not pages: a term confined to one page but repeated in\n * every section of it is still a specific term, and page-level counting would\n * flatten exactly the distinction the weight exists to make.\n */\nlet documentFrequency: Map<string, number> | null = null\n\nexport function docsDocumentFrequency(): {\n frequency: ReadonlyMap<string, number>\n sections: number\n} {\n if (!documentFrequency) {\n const index = (indexed ??= buildIndex(DOCS_SECTION_INDEX))\n const frequency = new Map<string, number>()\n for (const entry of index) {\n const seen = new Set<string>([\n ...entry.bodyTokenCounts.keys(),\n ...entry.titleTokens,\n ...entry.headingTokens,\n ])\n for (const token of seen) {\n frequency.set(token, (frequency.get(token) ?? 0) + 1)\n }\n }\n documentFrequency = frequency\n }\n return { frequency: documentFrequency, sections: DOCS_SECTION_INDEX.length }\n}\n\n/**\n * Public docs-site origin for deep links the assistant hands the user.\n *\n * Configurable (AGL-2016): a self-hoster who publishes the `apps/docs` site on\n * their own domain otherwise had every Assist citation deep-link into\n * `docs.aglyn.com` — our documentation, describing our deployment's flags and\n * plans, cited as if it were theirs.\n *\n * Unlike the operator CONTACT values this keeps our value as the default, and\n * the difference is deliberate: an unset citation origin degrades to a page\n * that genuinely exists and is genuinely about this software, so it is wrong\n * in the way a stale bookmark is wrong. An unset contact address would\n * misroute a legal notice, which is why that one refuses to guess.\n *\n * ⚠️ RE-EXPORTED FROM `DOCS_BASE_URL` RATHER THAN READ AGAIN HERE (AGL-2014).\n * This module had its own `process.env.NEXT_PUBLIC_DOCS_ORIGIN || 'https://\n * docs.aglyn.com'`, which accepted ONLY the canonical name — while\n * `constants/docs-links.ts` also honours the older `NEXT_PUBLIC_AGLYN_DOCS_URL`.\n * So AGL-2186's own defect survived, inverted, in the one file it did not\n * touch: a deployment configured under the older name retargeted every console\n * and besigner help link and left every ASSIST CITATION pointing at\n * `docs.aglyn.com` — our documentation, cited inside their product as theirs.\n * That is the same \"one value, two env names\" split AGL-733 is the record of.\n * One reader means an operator's docs site cannot be honoured on some surfaces\n * and ignored on others.\n */\nexport const DOCS_SITE_ORIGIN = DOCS_BASE_URL\n\n/**\n * The grounding block injected into the system prompt: the retrieved\n * sections, each with its citable URL, wrapped so the model can quote and\n * link them. Kept AFTER the stable system prefix so prompt caching of the\n * static prefix survives per-question variation.\n */\nexport function docsGroundingBlock(scored: readonly ScoredSection[]): string {\n if (!scored.length) return ''\n const parts = scored.map(({ section }) => {\n const url = `${DOCS_SITE_ORIGIN}${section.path}${section.anchor}`\n const heading = section.heading\n ? `${section.title} — ${section.heading}`\n : section.title\n return `<doc url=\"${url}\" title=\"${heading}\">\\n${section.text}\\n</doc>`\n })\n return `\\n\\nRelevant documentation sections (cite these URLs when they answer the question):\\n${parts.join('\\n')}`\n}\n"],"names":["DOCS_SECTION_INDEX","DOCS_BASE_URL","STOP_WORDS","Set","tokenize","text","toLowerCase","match","filter","token","has","scoreSection","section","queryTokens","bodyTokenCounts","titleTokens","headingTokens","score","tf","get","indexed","buildIndex","sections","map","Map","set","title","heading","retrieveDocsSections","question","limit","length","index","entry","scored","sort","a","b","slice","documentFrequency","docsDocumentFrequency","frequency","seen","keys","DOCS_SITE_ORIGIN","docsGroundingBlock","parts","url","path","anchor","join"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/docs-retrieval.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\nimport {\n DOCS_SECTION_INDEX,\n type DocsSection,\n} from './docs-index.generated'\nimport { DOCS_BASE_URL } from './docs-help'\n\n/**\n * Lexical docs search (AGL-1860, phase 1) — deliberately no vector store: a\n * BM25-flavoured term-frequency score over the generated docs section index,\n * with title/heading term boosts. Good enough to find the right docs section\n * for a \"how do I…\" question and cheap enough to run on every request inside\n * an API route.\n *\n * The platform's, and named so (AGL-3080): the console's issue reports\n * deflect to it with no model involved, and the AI plugin grounds its answers\n * in it — one reader among others, not its owner.\n */\n\n/** Words too common in this corpus to carry signal. */\nconst STOP_WORDS = new Set([\n 'a', 'an', 'and', 'are', 'as', 'at', 'be', 'by', 'can', 'do', 'does',\n 'for', 'from', 'how', 'i', 'in', 'is', 'it', 'my', 'of', 'on', 'or',\n 'that', 'the', 'this', 'to', 'what', 'when', 'where', 'which', 'why',\n 'with', 'you', 'your',\n])\n\n/** Lowercase word tokens, stop words removed, length >= 2. */\nexport function tokenize(text: string): string[] {\n return (text.toLowerCase().match(/[\\p{L}\\p{N}]{2,}/gu) ?? []).filter(\n (token) => !STOP_WORDS.has(token),\n )\n}\n\nexport interface ScoredSection {\n section: DocsSection\n score: number\n}\n\n/**\n * Score one section against query tokens. Term frequency saturates quickly\n * (BM25-ish k=1.2) so one section mentioning \"domain\" ten times does not\n * drown a section about the actual task; title and heading hits carry\n * dedicated boosts because those are the docs' own statement of topic.\n */\nfunction scoreSection(\n section: DocsSection,\n queryTokens: readonly string[],\n bodyTokenCounts: Map<string, number>,\n titleTokens: Set<string>,\n headingTokens: Set<string>,\n): number {\n let score = 0\n for (const token of queryTokens) {\n const tf = bodyTokenCounts.get(token) ?? 0\n if (tf > 0) score += (tf * 2.2) / (tf + 1.2)\n if (titleTokens.has(token)) score += 2.5\n if (headingTokens.has(token)) score += 2\n }\n return score\n}\n\ninterface IndexedSection {\n section: DocsSection\n bodyTokenCounts: Map<string, number>\n titleTokens: Set<string>\n headingTokens: Set<string>\n}\n\n/** Tokenized once per process — module scope, ~650 sections. */\nlet indexed: IndexedSection[] | null = null\n\nfunction buildIndex(sections: readonly DocsSection[]): IndexedSection[] {\n return sections.map((section) => {\n const bodyTokenCounts = new Map<string, number>()\n for (const token of tokenize(section.text)) {\n bodyTokenCounts.set(token, (bodyTokenCounts.get(token) ?? 0) + 1)\n }\n return {\n section,\n bodyTokenCounts,\n titleTokens: new Set(tokenize(section.title)),\n headingTokens: new Set(tokenize(section.heading)),\n }\n })\n}\n\n/**\n * Top-`limit` docs sections for a user question. Returns an empty array when\n * nothing scores above zero — the caller then answers without docs grounding\n * rather than citing an irrelevant page.\n */\nexport function retrieveDocsSections(\n question: string,\n limit = 6,\n sections: readonly DocsSection[] = DOCS_SECTION_INDEX,\n): ScoredSection[] {\n const queryTokens = [...new Set(tokenize(question))]\n if (!queryTokens.length) return []\n const index =\n sections === DOCS_SECTION_INDEX\n ? (indexed ??= buildIndex(sections))\n : buildIndex(sections)\n return index\n .map((entry) => ({\n section: entry.section,\n score: scoreSection(\n entry.section,\n queryTokens,\n entry.bodyTokenCounts,\n entry.titleTokens,\n entry.headingTokens,\n ),\n }))\n .filter((scored) => scored.score > 0)\n .sort((a, b) => b.score - a.score)\n .slice(0, limit)\n}\n\n/**\n * How many indexed sections each token appears in — the raw material for an\n * IDF weight (AGL-2486).\n *\n * Lives here rather than in the deflection module because the tokenized index\n * is already built here and rebuilding it there would tokenize ~500KB of docs\n * a second time, per process, to reach the same numbers. Memoised alongside\n * `indexed` and derived from it, so the two can never disagree about what a\n * token is.\n *\n * Counts SECTIONS, not pages: a term confined to one page but repeated in\n * every section of it is still a specific term, and page-level counting would\n * flatten exactly the distinction the weight exists to make.\n */\nlet documentFrequency: Map<string, number> | null = null\n\nexport function docsDocumentFrequency(): {\n frequency: ReadonlyMap<string, number>\n sections: number\n} {\n if (!documentFrequency) {\n const index = (indexed ??= buildIndex(DOCS_SECTION_INDEX))\n const frequency = new Map<string, number>()\n for (const entry of index) {\n const seen = new Set<string>([\n ...entry.bodyTokenCounts.keys(),\n ...entry.titleTokens,\n ...entry.headingTokens,\n ])\n for (const token of seen) {\n frequency.set(token, (frequency.get(token) ?? 0) + 1)\n }\n }\n documentFrequency = frequency\n }\n return { frequency: documentFrequency, sections: DOCS_SECTION_INDEX.length }\n}\n\n/** A letter of any script but Latin: Arabic, Hebrew, Cyrillic, CJK, Devanagari… */\nconst NON_LATIN_LETTER = /[^\\P{L}\\p{Script=Latin}]/u\n\n/**\n * Whether a question has to be put into English before the docs can be\n * searched for it (AGL-4075).\n *\n * The docs are written in English and the search matches words, so a\n * question in another language matches nothing by its own words: 17 of 50\n * chat turns in one fortnight were not in English, and none of them was\n * grounded in the docs at all. True for a question with a letter outside the\n * Latin script; for a Latin-script question fewer than half of whose words\n * appear anywhere in the docs (\"¿Cómo conecto mi dominio?\"); and for one\n * with a word the docs never use and none of English's small words (\"how\",\n * \"do\", \"my\", \"the\"), which is how a question in French or Portuguese reads\n * when it shares half its words with English (\"Comment publier mon site ?\").\n * An English question about the product uses the docs' own words, so it is\n * searched as it was asked. A question with no words has nothing to look up.\n */\nexport function docsLookupNeedsEnglish(question: string): boolean {\n const text = String(question ?? '')\n if (NON_LATIN_LETTER.test(text)) return true\n const tokens = tokenize(text)\n if (!tokens.length) return false\n const { frequency } = docsDocumentFrequency()\n const unknown = tokens.filter((token) => !frequency.has(token)).length\n if (unknown * 2 > tokens.length) return true\n const words = text.toLowerCase().match(/[\\p{L}\\p{N}]{2,}/gu) ?? []\n return unknown > 0 && !words.some((word) => STOP_WORDS.has(word))\n}\n\n/**\n * Public docs-site origin for deep links the assistant hands the user.\n *\n * Configurable (AGL-2016): a self-hoster who publishes the `apps/docs` site on\n * their own domain otherwise had every Assist citation deep-link into\n * `docs.aglyn.com` — our documentation, describing our deployment's flags and\n * plans, cited as if it were theirs.\n *\n * Unlike the operator CONTACT values this keeps our value as the default, and\n * the difference is deliberate: an unset citation origin degrades to a page\n * that genuinely exists and is genuinely about this software, so it is wrong\n * in the way a stale bookmark is wrong. An unset contact address would\n * misroute a legal notice, which is why that one refuses to guess.\n *\n * ⚠️ RE-EXPORTED FROM `DOCS_BASE_URL` RATHER THAN READ AGAIN HERE (AGL-2014).\n * This module had its own `process.env.NEXT_PUBLIC_DOCS_ORIGIN || 'https://\n * docs.aglyn.com'`, which accepted ONLY the canonical name — while\n * `constants/docs-links.ts` also honours the older `NEXT_PUBLIC_AGLYN_DOCS_URL`.\n * So AGL-2186's own defect survived, inverted, in the one file it did not\n * touch: a deployment configured under the older name retargeted every console\n * and besigner help link and left every ASSIST CITATION pointing at\n * `docs.aglyn.com` — our documentation, cited inside their product as theirs.\n * That is the same \"one value, two env names\" split AGL-733 is the record of.\n * One reader means an operator's docs site cannot be honoured on some surfaces\n * and ignored on others.\n */\nexport const DOCS_SITE_ORIGIN = DOCS_BASE_URL\n\n/**\n * The grounding block injected into the system prompt: the retrieved\n * sections, each with its citable URL, wrapped so the model can quote and\n * link them. Kept AFTER the stable system prefix so prompt caching of the\n * static prefix survives per-question variation.\n */\nexport function docsGroundingBlock(scored: readonly ScoredSection[]): string {\n if (!scored.length) return ''\n const parts = scored.map(({ section }) => {\n const url = `${DOCS_SITE_ORIGIN}${section.path}${section.anchor}`\n const heading = section.heading\n ? `${section.title} — ${section.heading}`\n : section.title\n return `<doc url=\"${url}\" title=\"${heading}\">\\n${section.text}\\n</doc>`\n })\n return `\\n\\nRelevant documentation sections (cite these URLs when they answer the question):\\n${parts.join('\\n')}`\n}\n"],"names":["DOCS_SECTION_INDEX","DOCS_BASE_URL","STOP_WORDS","Set","tokenize","text","toLowerCase","match","filter","token","has","scoreSection","section","queryTokens","bodyTokenCounts","titleTokens","headingTokens","score","tf","get","indexed","buildIndex","sections","map","Map","set","title","heading","retrieveDocsSections","question","limit","length","index","entry","scored","sort","a","b","slice","documentFrequency","docsDocumentFrequency","frequency","seen","keys","NON_LATIN_LETTER","docsLookupNeedsEnglish","String","test","tokens","unknown","words","some","word","DOCS_SITE_ORIGIN","docsGroundingBlock","parts","url","path","anchor","join"],"mappings":"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"}
@@ -35,7 +35,8 @@ export declare const RESERVED_SUBDOMAINS: Set<string>;
35
35
  */
36
36
  export declare function consoleLabelUnder(consoleUrl: string | null | undefined, workspaceDomain: string | null | undefined): string | null;
37
37
  /**
38
- * True when the subdomain is reserved, contains a blocked fragment, or wears
38
+ * True when the subdomain is reserved, is a numbered or suffixed variant of an
39
+ * infrastructure word (AGL-4082), contains a blocked fragment, or wears
39
40
  * a brand a phishing kit impersonates (AGL-3356).
40
41
  *
41
42
  * The brand check is the phishing screen's own ({@link brandForSubdomainLabel}):
@@ -51,8 +51,69 @@ import { TENANT_APEX } from "./tenant-apex.js";
51
51
  'account',
52
52
  'login',
53
53
  'signup',
54
- 'auth'
54
+ 'auth',
55
+ // Account, mail and domain words an address can hide behind (AGL-4082).
56
+ 'accounts',
57
+ 'signin',
58
+ 'sso',
59
+ 'secure',
60
+ 'verify',
61
+ 'portal',
62
+ 'webmail',
63
+ 'imap',
64
+ 'mx',
65
+ 'com',
66
+ 'net',
67
+ 'org'
55
68
  ]);
69
+ /**
70
+ * Infrastructure and account words whose numbered or suffixed variants are
71
+ * refused too (AGL-4082): `www-2`, `www2`, `mail1`, `login-secure`, `app-online`
72
+ * read as one of our own servers. Product words such as `demo`, `test` and
73
+ * `blog` stay out, so `demo-2` is still a fine bakery's address.
74
+ */ const INFRASTRUCTURE_WORDS = [
75
+ 'www',
76
+ 'app',
77
+ 'api',
78
+ 'admin',
79
+ 'console',
80
+ 'mail',
81
+ 'webmail',
82
+ 'smtp',
83
+ 'imap',
84
+ 'mx',
85
+ 'ns',
86
+ 'ftp',
87
+ 'cdn',
88
+ 'static',
89
+ 'assets',
90
+ 'status',
91
+ 'aglyn',
92
+ 'billing',
93
+ 'account',
94
+ 'accounts',
95
+ 'login',
96
+ 'signin',
97
+ 'signup',
98
+ 'auth',
99
+ 'sso',
100
+ 'secure',
101
+ 'verify',
102
+ 'portal',
103
+ 'support'
104
+ ];
105
+ /** The suffixes that turn an infrastructure word into a lookalike address. */ const INFRASTRUCTURE_SUFFIXES = [
106
+ 'site',
107
+ 'web',
108
+ 'online',
109
+ 'secure',
110
+ 'login',
111
+ 'portal',
112
+ 'official',
113
+ 'verify',
114
+ 'page'
115
+ ];
116
+ const INFRASTRUCTURE_VARIANT = new RegExp(`^(?:www\\d*-.*|(?:${INFRASTRUCTURE_WORDS.join('|')})(?:-?\\d+|-(?:${INFRASTRUCTURE_SUFFIXES.join('|')})))$`);
56
117
  /**
57
118
  * Profanity fragments blocked as substrings in generated or typed
58
119
  * subdomains. Deliberately short: obvious slurs and vulgarity only —
@@ -95,7 +156,8 @@ import { TENANT_APEX } from "./tenant-apex.js";
95
156
  return label && !label.includes('.') ? label : null;
96
157
  }
97
158
  /**
98
- * True when the subdomain is reserved, contains a blocked fragment, or wears
159
+ * True when the subdomain is reserved, is a numbered or suffixed variant of an
160
+ * infrastructure word (AGL-4082), contains a blocked fragment, or wears
99
161
  * a brand a phishing kit impersonates (AGL-3356).
100
162
  *
101
163
  * The brand check is the phishing screen's own ({@link brandForSubdomainLabel}):
@@ -108,6 +170,7 @@ import { TENANT_APEX } from "./tenant-apex.js";
108
170
  */ export function isBlockedSubdomain(subdomain) {
109
171
  const normalized = subdomain.toLowerCase();
110
172
  if (RESERVED_SUBDOMAINS.has(normalized)) return true;
173
+ if (INFRASTRUCTURE_VARIANT.test(normalized)) return true;
111
174
  const collapsed = normalized.replace(/[^a-z0-9]/g, '');
112
175
  if (BLOCKED_FRAGMENTS.some((fragment)=>collapsed.includes(fragment))) return true;
113
176
  return brandForSubdomainLabel(normalized) !== null;
@@ -1 +1 @@
1
- {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-naming.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 * Host naming guards (AGL-147): one shared subdomain policy for the create\n * API, the rename/validate API, and the console dialogs — pattern, reserved\n * + profanity blocklists, a display-name → subdomain generator, and\n * taken-name suggestions.\n */\n\n// The leaf, not the barrel: the brand list and lookalike logic every\n// phishing surface reads (AGL-3356), and nothing else of the email library.\nimport { brandForSubdomainLabel } from '@aglyn/shared-util-email/outbound-phishing-screen'\nimport { TENANT_APEX } from './tenant-apex'\n\n/** 3–30 chars, lowercase alphanumeric + dashes, no leading dash. */\nexport const SUBDOMAIN_PATTERN = /^[a-z0-9][a-z0-9-]{2,29}$/\n\n/** Platform/system names a tenant must not squat. */\nexport const RESERVED_SUBDOMAINS = new Set([\n 'www',\n 'app',\n 'api',\n 'admin',\n 'console',\n 'mail',\n 'demo',\n 'staging',\n 'dev',\n 'test',\n 'docs',\n 'blog',\n 'help',\n 'support',\n 'status',\n 'cdn',\n 'assets',\n 'static',\n 'ftp',\n 'smtp',\n 'ns1',\n 'ns2',\n 'aglyn',\n 'billing',\n 'account',\n 'login',\n 'signup',\n 'auth',\n])\n\n/**\n * Profanity fragments blocked as substrings in generated or typed\n * subdomains. Deliberately short: obvious slurs and vulgarity only —\n * substring matching is aggressive, so entries must be unambiguous.\n */\nconst BLOCKED_FRAGMENTS = [\n 'fuck',\n 'shit',\n 'cunt',\n 'nigger',\n 'faggot',\n 'bitch',\n 'porn',\n 'nazi',\n]\n\n/**\n * The one label a console URL occupies directly under a workspace domain\n * (AGL-3295): `studio` for `https://studio.example.com` under `example.com`.\n *\n * `null` when either setting is missing or unparseable, and when the console\n * is the apex itself, sits deeper, or lies outside the domain — none of those\n * can collide with a workspace address, so none needs reserving.\n *\n * Pure, and in this dependency-free module, so the console's host gate and the\n * organization-slug blocklist derive the same label from the same two\n * settings. An install whose console is not at `app.` or `console.` is the\n * case it exists for: without it, that label is read as a workspace nobody\n * owns.\n */\nexport function consoleLabelUnder(\n consoleUrl: string | null | undefined,\n workspaceDomain: string | null | undefined,\n): string | null {\n const domain = String(workspaceDomain ?? '')\n .trim()\n .toLowerCase()\n const raw = String(consoleUrl ?? '').trim()\n if (!domain || !raw) return null\n let hostname: string\n try {\n hostname = new URL(raw).hostname.toLowerCase()\n } catch {\n return null\n }\n if (!hostname.endsWith(`.${domain}`)) return null\n const label = hostname.slice(0, -(domain.length + 1))\n return label && !label.includes('.') ? label : null\n}\n\n/**\n * True when the subdomain is reserved, contains a blocked fragment, or wears\n * a brand a phishing kit impersonates (AGL-3356).\n *\n * The brand check is the phishing screen's own ({@link brandForSubdomainLabel}):\n * a brand's name or a lookalike of it (`poshmark`, `paypal-secure`,\n * `paypa1`, `appleid`), or a brand's word joined to an account word\n * (`booking-review`, `apple-support`). Refused for every workspace at create\n * and at rename, since a brand's name on our apex is a disguise that\n * outlives every later screen — while a real business's name that merely\n * contains a common word (`tanyas-booking`, `apple-pie-co`) is not.\n */\nexport function isBlockedSubdomain(subdomain: string): boolean {\n const normalized = subdomain.toLowerCase()\n if (RESERVED_SUBDOMAINS.has(normalized)) return true\n const collapsed = normalized.replace(/[^a-z0-9]/g, '')\n if (BLOCKED_FRAGMENTS.some((fragment) => collapsed.includes(fragment))) return true\n return brandForSubdomainLabel(normalized) !== null\n}\n\n/**\n * Best-effort subdomain from a display name: lowercase, spaces/symbols to\n * dashes, collapsed, trimmed to the pattern's 30-char cap. Returns '' when\n * nothing usable remains (caller falls back to manual entry).\n */\nexport function generateSubdomain(displayName: string): string {\n const slug = displayName\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/-{2,}/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 30)\n .replace(/-+$/, '')\n if (!SUBDOMAIN_PATTERN.test(slug) || isBlockedSubdomain(slug)) return ''\n return slug\n}\n\n/**\n * Alternatives for a taken subdomain: `name-2`, `name-<year>`, `name-site`.\n * Candidates are truncated to fit the pattern; blocked names are dropped.\n * Availability is the caller's job (needs a Firestore query).\n */\nexport function suggestSubdomains(base: string, year = new Date().getFullYear()): string[] {\n const stem = base.toLowerCase().replace(/-+$/, '')\n const withSuffix = (suffix: string) =>\n `${stem.slice(0, 30 - suffix.length - 1)}-${suffix}`.replace(/-{2,}/g, '-')\n const candidates = [withSuffix('2'), withSuffix(String(year)), withSuffix('site')]\n return [\n ...new Set(\n candidates.filter(\n (candidate) =>\n SUBDOMAIN_PATTERN.test(candidate) && !isBlockedSubdomain(candidate),\n ),\n ),\n ]\n}\n\n// The apex tenant sites are served from lives in its own leaf, so a published\n// page can read it without the subdomain policy above (AGL-3401). Re-exported\n// here so every importer of it keeps working.\nexport { TENANT_APEX }\n\n/**\n * The absolute origin a published site is reachable at (AGL-1224).\n *\n * A custom domain wins over the platform subdomain, matching what the tenant\n * renderer already emits as `<link rel=\"canonical\">` — a site on a custom\n * domain that advertised its `.aglyn.app` origin would be describing itself\n * by a name its visitors never see.\n *\n * Returns undefined rather than a half-built URL when a host has neither, so\n * a caller that needs an absolute URL can tell it does not have one. Emails\n * are the sharp case: an image `src` that is site-relative resolves against\n * nothing in an inbox, so \"no origin\" has to be answerable.\n */\nexport function hostPublicOrigin(\n host:\n | { cname?: string | null; subdomain?: string | null }\n | null\n | undefined,\n): string | undefined {\n if (host?.cname) return `https://${host.cname}`\n if (host?.subdomain) return `https://${host.subdomain}.${TENANT_APEX}`\n return undefined\n}\n\n/**\n * The custom-domain lifecycle as it is actually persisted on the host doc.\n *\n * There is no `verifiedAt`, no `status`, no `sslReady` — the console's\n * `/api/domains/*` routes write exactly these three fields, so this is the\n * whole vocabulary anything downstream has to reason from.\n */\nexport interface HostCustomDomainState {\n cname?: string | null\n /** Vercel attach failed or was unconfigured — the domain does NOT serve. */\n cnameAttachmentPending?: boolean | null\n /** Vercel detach failed — the customer is mid-disconnect. */\n cnameDetachmentPending?: boolean | null\n subdomain?: string | null\n}\n\n/** A bare hostname: labels of a-z0-9/dash, at least two of them. */\nconst CUSTOM_DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(\\.(?!-)[a-z0-9-]{1,63})+$/\n\n/**\n * The custom domain a host is actually **serving on**, or undefined (AGL-1272).\n *\n * Deliberately stricter than `hostPublicOrigin`, which answers \"what name does\n * this site call itself\" and is right to prefer `cname` the moment it exists.\n * This answers a load-bearing question instead — \"is it safe to send visitors\n * away from the platform subdomain and onto this name\" — and the cost of\n * getting it wrong is the whole site, not a wrong URL in an email.\n *\n * So every reason to doubt the domain is a refusal:\n *\n * - `cnameAttachmentPending` means the Vercel attach never landed (missing\n * env, or a 5xx). The domain is claimed in Firestore and serves nothing:\n * no certificate, no routing. This is precisely the \"attached but not\n * live\" state, and redirecting to it strands the site on a dead host.\n * - `cnameDetachmentPending` means the customer asked to disconnect and the\n * platform release failed. The name may still resolve, but the customer's\n * stated intent is to leave it, so we stop advertising it.\n * - A name inside `aglyn.app` is refused outright. `{sub}.aglyn.app` is the\n * very origin we redirect FROM, and the tenant middleware resolves that\n * space by `subdomain` before it ever considers `cname` — so a host whose\n * cname pointed back into it would redirect to itself forever.\n * - A name that is not hostname-shaped is refused. `/api/domains/attach`\n * lowercases and trims what the wizard sends but never pattern-checks it\n * (only `/api/domains/verify` does), so a junk value can reach Firestore.\n * A junk value reaching a `Location:` header is a different class of\n * problem again.\n *\n * Note what this CANNOT tell you: DNS was verified once, at connect time, and\n * nothing re-checks it. A domain whose DNS is later repointed or whose\n * registration lapses still reads as live here. That residual risk is why the\n * redirect built on this is temporary and revocable rather than permanent.\n */\nexport function liveCustomDomain(\n host: HostCustomDomainState | null | undefined,\n): string | undefined {\n if (!host) return undefined\n if (host.cnameAttachmentPending === true) return undefined\n if (host.cnameDetachmentPending === true) return undefined\n const domain = String(host.cname ?? '')\n .trim()\n .toLowerCase()\n if (!CUSTOM_DOMAIN_PATTERN.test(domain)) return undefined\n if (domain === TENANT_APEX || domain.endsWith(`.${TENANT_APEX}`)) {\n return undefined\n }\n return domain\n}\n"],"names":["brandForSubdomainLabel","TENANT_APEX","SUBDOMAIN_PATTERN","RESERVED_SUBDOMAINS","Set","BLOCKED_FRAGMENTS","consoleLabelUnder","consoleUrl","workspaceDomain","domain","String","trim","toLowerCase","raw","hostname","URL","endsWith","label","slice","length","includes","isBlockedSubdomain","subdomain","normalized","has","collapsed","replace","some","fragment","generateSubdomain","displayName","slug","normalize","test","suggestSubdomains","base","year","Date","getFullYear","stem","withSuffix","suffix","candidates","filter","candidate","hostPublicOrigin","host","cname","undefined","CUSTOM_DOMAIN_PATTERN","liveCustomDomain","cnameAttachmentPending","cnameDetachmentPending"],"mappings":"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"}
1
+ {"version":3,"sources":["../../../../../../libs/aglyn/src/lib/app-utils/host-naming.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 * Host naming guards (AGL-147): one shared subdomain policy for the create\n * API, the rename/validate API, and the console dialogs — pattern, reserved\n * + profanity blocklists, a display-name → subdomain generator, and\n * taken-name suggestions.\n */\n\n// The leaf, not the barrel: the brand list and lookalike logic every\n// phishing surface reads (AGL-3356), and nothing else of the email library.\nimport { brandForSubdomainLabel } from '@aglyn/shared-util-email/outbound-phishing-screen'\nimport { TENANT_APEX } from './tenant-apex'\n\n/** 3–30 chars, lowercase alphanumeric + dashes, no leading dash. */\nexport const SUBDOMAIN_PATTERN = /^[a-z0-9][a-z0-9-]{2,29}$/\n\n/** Platform/system names a tenant must not squat. */\nexport const RESERVED_SUBDOMAINS = new Set([\n 'www',\n 'app',\n 'api',\n 'admin',\n 'console',\n 'mail',\n 'demo',\n 'staging',\n 'dev',\n 'test',\n 'docs',\n 'blog',\n 'help',\n 'support',\n 'status',\n 'cdn',\n 'assets',\n 'static',\n 'ftp',\n 'smtp',\n 'ns1',\n 'ns2',\n 'aglyn',\n 'billing',\n 'account',\n 'login',\n 'signup',\n 'auth',\n // Account, mail and domain words an address can hide behind (AGL-4082).\n 'accounts',\n 'signin',\n 'sso',\n 'secure',\n 'verify',\n 'portal',\n 'webmail',\n 'imap',\n 'mx',\n 'com',\n 'net',\n 'org',\n])\n\n/**\n * Infrastructure and account words whose numbered or suffixed variants are\n * refused too (AGL-4082): `www-2`, `www2`, `mail1`, `login-secure`, `app-online`\n * read as one of our own servers. Product words such as `demo`, `test` and\n * `blog` stay out, so `demo-2` is still a fine bakery's address.\n */\nconst INFRASTRUCTURE_WORDS = [\n 'www',\n 'app',\n 'api',\n 'admin',\n 'console',\n 'mail',\n 'webmail',\n 'smtp',\n 'imap',\n 'mx',\n 'ns',\n 'ftp',\n 'cdn',\n 'static',\n 'assets',\n 'status',\n 'aglyn',\n 'billing',\n 'account',\n 'accounts',\n 'login',\n 'signin',\n 'signup',\n 'auth',\n 'sso',\n 'secure',\n 'verify',\n 'portal',\n 'support',\n]\n\n/** The suffixes that turn an infrastructure word into a lookalike address. */\nconst INFRASTRUCTURE_SUFFIXES = [\n 'site',\n 'web',\n 'online',\n 'secure',\n 'login',\n 'portal',\n 'official',\n 'verify',\n 'page',\n]\n\nconst INFRASTRUCTURE_VARIANT = new RegExp(\n `^(?:www\\\\d*-.*|(?:${INFRASTRUCTURE_WORDS.join('|')})(?:-?\\\\d+|-(?:${INFRASTRUCTURE_SUFFIXES.join('|')})))$`,\n)\n\n/**\n * Profanity fragments blocked as substrings in generated or typed\n * subdomains. Deliberately short: obvious slurs and vulgarity only —\n * substring matching is aggressive, so entries must be unambiguous.\n */\nconst BLOCKED_FRAGMENTS = [\n 'fuck',\n 'shit',\n 'cunt',\n 'nigger',\n 'faggot',\n 'bitch',\n 'porn',\n 'nazi',\n]\n\n/**\n * The one label a console URL occupies directly under a workspace domain\n * (AGL-3295): `studio` for `https://studio.example.com` under `example.com`.\n *\n * `null` when either setting is missing or unparseable, and when the console\n * is the apex itself, sits deeper, or lies outside the domain — none of those\n * can collide with a workspace address, so none needs reserving.\n *\n * Pure, and in this dependency-free module, so the console's host gate and the\n * organization-slug blocklist derive the same label from the same two\n * settings. An install whose console is not at `app.` or `console.` is the\n * case it exists for: without it, that label is read as a workspace nobody\n * owns.\n */\nexport function consoleLabelUnder(\n consoleUrl: string | null | undefined,\n workspaceDomain: string | null | undefined,\n): string | null {\n const domain = String(workspaceDomain ?? '')\n .trim()\n .toLowerCase()\n const raw = String(consoleUrl ?? '').trim()\n if (!domain || !raw) return null\n let hostname: string\n try {\n hostname = new URL(raw).hostname.toLowerCase()\n } catch {\n return null\n }\n if (!hostname.endsWith(`.${domain}`)) return null\n const label = hostname.slice(0, -(domain.length + 1))\n return label && !label.includes('.') ? label : null\n}\n\n/**\n * True when the subdomain is reserved, is a numbered or suffixed variant of an\n * infrastructure word (AGL-4082), contains a blocked fragment, or wears\n * a brand a phishing kit impersonates (AGL-3356).\n *\n * The brand check is the phishing screen's own ({@link brandForSubdomainLabel}):\n * a brand's name or a lookalike of it (`poshmark`, `paypal-secure`,\n * `paypa1`, `appleid`), or a brand's word joined to an account word\n * (`booking-review`, `apple-support`). Refused for every workspace at create\n * and at rename, since a brand's name on our apex is a disguise that\n * outlives every later screen — while a real business's name that merely\n * contains a common word (`tanyas-booking`, `apple-pie-co`) is not.\n */\nexport function isBlockedSubdomain(subdomain: string): boolean {\n const normalized = subdomain.toLowerCase()\n if (RESERVED_SUBDOMAINS.has(normalized)) return true\n if (INFRASTRUCTURE_VARIANT.test(normalized)) return true\n const collapsed = normalized.replace(/[^a-z0-9]/g, '')\n if (BLOCKED_FRAGMENTS.some((fragment) => collapsed.includes(fragment))) return true\n return brandForSubdomainLabel(normalized) !== null\n}\n\n/**\n * Best-effort subdomain from a display name: lowercase, spaces/symbols to\n * dashes, collapsed, trimmed to the pattern's 30-char cap. Returns '' when\n * nothing usable remains (caller falls back to manual entry).\n */\nexport function generateSubdomain(displayName: string): string {\n const slug = displayName\n .toLowerCase()\n .normalize('NFKD')\n .replace(/[̀-ͯ]/g, '')\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/-{2,}/g, '-')\n .replace(/^-+|-+$/g, '')\n .slice(0, 30)\n .replace(/-+$/, '')\n if (!SUBDOMAIN_PATTERN.test(slug) || isBlockedSubdomain(slug)) return ''\n return slug\n}\n\n/**\n * Alternatives for a taken subdomain: `name-2`, `name-<year>`, `name-site`.\n * Candidates are truncated to fit the pattern; blocked names are dropped.\n * Availability is the caller's job (needs a Firestore query).\n */\nexport function suggestSubdomains(base: string, year = new Date().getFullYear()): string[] {\n const stem = base.toLowerCase().replace(/-+$/, '')\n const withSuffix = (suffix: string) =>\n `${stem.slice(0, 30 - suffix.length - 1)}-${suffix}`.replace(/-{2,}/g, '-')\n const candidates = [withSuffix('2'), withSuffix(String(year)), withSuffix('site')]\n return [\n ...new Set(\n candidates.filter(\n (candidate) =>\n SUBDOMAIN_PATTERN.test(candidate) && !isBlockedSubdomain(candidate),\n ),\n ),\n ]\n}\n\n// The apex tenant sites are served from lives in its own leaf, so a published\n// page can read it without the subdomain policy above (AGL-3401). Re-exported\n// here so every importer of it keeps working.\nexport { TENANT_APEX }\n\n/**\n * The absolute origin a published site is reachable at (AGL-1224).\n *\n * A custom domain wins over the platform subdomain, matching what the tenant\n * renderer already emits as `<link rel=\"canonical\">` — a site on a custom\n * domain that advertised its `.aglyn.app` origin would be describing itself\n * by a name its visitors never see.\n *\n * Returns undefined rather than a half-built URL when a host has neither, so\n * a caller that needs an absolute URL can tell it does not have one. Emails\n * are the sharp case: an image `src` that is site-relative resolves against\n * nothing in an inbox, so \"no origin\" has to be answerable.\n */\nexport function hostPublicOrigin(\n host:\n | { cname?: string | null; subdomain?: string | null }\n | null\n | undefined,\n): string | undefined {\n if (host?.cname) return `https://${host.cname}`\n if (host?.subdomain) return `https://${host.subdomain}.${TENANT_APEX}`\n return undefined\n}\n\n/**\n * The custom-domain lifecycle as it is actually persisted on the host doc.\n *\n * There is no `verifiedAt`, no `status`, no `sslReady` — the console's\n * `/api/domains/*` routes write exactly these three fields, so this is the\n * whole vocabulary anything downstream has to reason from.\n */\nexport interface HostCustomDomainState {\n cname?: string | null\n /** Vercel attach failed or was unconfigured — the domain does NOT serve. */\n cnameAttachmentPending?: boolean | null\n /** Vercel detach failed — the customer is mid-disconnect. */\n cnameDetachmentPending?: boolean | null\n subdomain?: string | null\n}\n\n/** A bare hostname: labels of a-z0-9/dash, at least two of them. */\nconst CUSTOM_DOMAIN_PATTERN = /^(?!-)[a-z0-9-]{1,63}(\\.(?!-)[a-z0-9-]{1,63})+$/\n\n/**\n * The custom domain a host is actually **serving on**, or undefined (AGL-1272).\n *\n * Deliberately stricter than `hostPublicOrigin`, which answers \"what name does\n * this site call itself\" and is right to prefer `cname` the moment it exists.\n * This answers a load-bearing question instead — \"is it safe to send visitors\n * away from the platform subdomain and onto this name\" — and the cost of\n * getting it wrong is the whole site, not a wrong URL in an email.\n *\n * So every reason to doubt the domain is a refusal:\n *\n * - `cnameAttachmentPending` means the Vercel attach never landed (missing\n * env, or a 5xx). The domain is claimed in Firestore and serves nothing:\n * no certificate, no routing. This is precisely the \"attached but not\n * live\" state, and redirecting to it strands the site on a dead host.\n * - `cnameDetachmentPending` means the customer asked to disconnect and the\n * platform release failed. The name may still resolve, but the customer's\n * stated intent is to leave it, so we stop advertising it.\n * - A name inside `aglyn.app` is refused outright. `{sub}.aglyn.app` is the\n * very origin we redirect FROM, and the tenant middleware resolves that\n * space by `subdomain` before it ever considers `cname` — so a host whose\n * cname pointed back into it would redirect to itself forever.\n * - A name that is not hostname-shaped is refused. `/api/domains/attach`\n * lowercases and trims what the wizard sends but never pattern-checks it\n * (only `/api/domains/verify` does), so a junk value can reach Firestore.\n * A junk value reaching a `Location:` header is a different class of\n * problem again.\n *\n * Note what this CANNOT tell you: DNS was verified once, at connect time, and\n * nothing re-checks it. A domain whose DNS is later repointed or whose\n * registration lapses still reads as live here. That residual risk is why the\n * redirect built on this is temporary and revocable rather than permanent.\n */\nexport function liveCustomDomain(\n host: HostCustomDomainState | null | undefined,\n): string | undefined {\n if (!host) return undefined\n if (host.cnameAttachmentPending === true) return undefined\n if (host.cnameDetachmentPending === true) return undefined\n const domain = String(host.cname ?? '')\n .trim()\n .toLowerCase()\n if (!CUSTOM_DOMAIN_PATTERN.test(domain)) return undefined\n if (domain === TENANT_APEX || domain.endsWith(`.${TENANT_APEX}`)) {\n return undefined\n }\n return domain\n}\n"],"names":["brandForSubdomainLabel","TENANT_APEX","SUBDOMAIN_PATTERN","RESERVED_SUBDOMAINS","Set","INFRASTRUCTURE_WORDS","INFRASTRUCTURE_SUFFIXES","INFRASTRUCTURE_VARIANT","RegExp","join","BLOCKED_FRAGMENTS","consoleLabelUnder","consoleUrl","workspaceDomain","domain","String","trim","toLowerCase","raw","hostname","URL","endsWith","label","slice","length","includes","isBlockedSubdomain","subdomain","normalized","has","test","collapsed","replace","some","fragment","generateSubdomain","displayName","slug","normalize","suggestSubdomains","base","year","Date","getFullYear","stem","withSuffix","suffix","candidates","filter","candidate","hostPublicOrigin","host","cname","undefined","CUSTOM_DOMAIN_PATTERN","liveCustomDomain","cnameAttachmentPending","cnameDetachmentPending"],"mappings":"AAAA;;;;;;;;;;;;;;;CAeC,GAED;;;;;CAKC,GAED,qEAAqE;AACrE,4EAA4E;AAC5E,SAASA,sBAAsB,QAAQ,oDAAmD;AAC1F,SAASC,WAAW,QAAQ,mBAAe;AAE3C,kEAAkE,GAClE,OAAO,MAAMC,oBAAoB,4BAA2B;AAE5D,mDAAmD,GACnD,OAAO,MAAMC,sBAAsB,IAAIC,IAAI;IACzC;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA,wEAAwE;IACxE;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD,EAAC;AAEF;;;;;CAKC,GACD,MAAMC,uBAAuB;IAC3B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED,4EAA4E,GAC5E,MAAMC,0BAA0B;IAC9B;IACA;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED,MAAMC,yBAAyB,IAAIC,OACjC,CAAC,kBAAkB,EAAEH,qBAAqBI,IAAI,CAAC,KAAK,eAAe,EAAEH,wBAAwBG,IAAI,CAAC,KAAK,IAAI,CAAC;AAG9G;;;;CAIC,GACD,MAAMC,oBAAoB;IACxB;IACA;IACA;IACA;IACA;IACA;IACA;IACA;CACD;AAED;;;;;;;;;;;;;CAaC,GACD,OAAO,SAASC,kBACdC,UAAqC,EACrCC,eAA0C;IAE1C,MAAMC,SAASC,OAAOF,0BAAAA,kBAAmB,IACtCG,IAAI,GACJC,WAAW;IACd,MAAMC,MAAMH,OAAOH,qBAAAA,aAAc,IAAII,IAAI;IACzC,IAAI,CAACF,UAAU,CAACI,KAAK,OAAO;IAC5B,IAAIC;IACJ,IAAI;QACFA,WAAW,IAAIC,IAAIF,KAAKC,QAAQ,CAACF,WAAW;IAC9C,EAAE,eAAM;QACN,OAAO;IACT;IACA,IAAI,CAACE,SAASE,QAAQ,CAAC,CAAC,CAAC,EAAEP,QAAQ,GAAG,OAAO;IAC7C,MAAMQ,QAAQH,SAASI,KAAK,CAAC,GAAG,CAAET,CAAAA,OAAOU,MAAM,GAAG,CAAA;IAClD,OAAOF,SAAS,CAACA,MAAMG,QAAQ,CAAC,OAAOH,QAAQ;AACjD;AAEA;;;;;;;;;;;;CAYC,GACD,OAAO,SAASI,mBAAmBC,SAAiB;IAClD,MAAMC,aAAaD,UAAUV,WAAW;IACxC,IAAId,oBAAoB0B,GAAG,CAACD,aAAa,OAAO;IAChD,IAAIrB,uBAAuBuB,IAAI,CAACF,aAAa,OAAO;IACpD,MAAMG,YAAYH,WAAWI,OAAO,CAAC,cAAc;IACnD,IAAItB,kBAAkBuB,IAAI,CAAC,CAACC,WAAaH,UAAUN,QAAQ,CAACS,YAAY,OAAO;IAC/E,OAAOlC,uBAAuB4B,gBAAgB;AAChD;AAEA;;;;CAIC,GACD,OAAO,SAASO,kBAAkBC,WAAmB;IACnD,MAAMC,OAAOD,YACVnB,WAAW,GACXqB,SAAS,CAAC,QACVN,OAAO,CAAC,UAAU,IAClBA,OAAO,CAAC,eAAe,KACvBA,OAAO,CAAC,UAAU,KAClBA,OAAO,CAAC,YAAY,IACpBT,KAAK,CAAC,GAAG,IACTS,OAAO,CAAC,OAAO;IAClB,IAAI,CAAC9B,kBAAkB4B,IAAI,CAACO,SAASX,mBAAmBW,OAAO,OAAO;IACtE,OAAOA;AACT;AAEA;;;;CAIC,GACD,OAAO,SAASE,kBAAkBC,IAAY,EAAEC,OAAO,IAAIC,OAAOC,WAAW,EAAE;IAC7E,MAAMC,OAAOJ,KAAKvB,WAAW,GAAGe,OAAO,CAAC,OAAO;IAC/C,MAAMa,aAAa,CAACC,SAClB,GAAGF,KAAKrB,KAAK,CAAC,GAAG,KAAKuB,OAAOtB,MAAM,GAAG,GAAG,CAAC,EAAEsB,QAAQ,CAACd,OAAO,CAAC,UAAU;IACzE,MAAMe,aAAa;QAACF,WAAW;QAAMA,WAAW9B,OAAO0B;QAAQI,WAAW;KAAQ;IAClF,OAAO;WACF,IAAIzC,IACL2C,WAAWC,MAAM,CACf,CAACC,YACC/C,kBAAkB4B,IAAI,CAACmB,cAAc,CAACvB,mBAAmBuB;KAGhE;AACH;AAEA,8EAA8E;AAC9E,8EAA8E;AAC9E,8CAA8C;AAC9C,SAAShD,WAAW,GAAE;AAEtB;;;;;;;;;;;;CAYC,GACD,OAAO,SAASiD,iBACdC,IAGa;IAEb,IAAIA,wBAAAA,KAAMC,KAAK,EAAE,OAAO,CAAC,QAAQ,EAAED,KAAKC,KAAK,EAAE;IAC/C,IAAID,wBAAAA,KAAMxB,SAAS,EAAE,OAAO,CAAC,QAAQ,EAAEwB,KAAKxB,SAAS,CAAC,CAAC,EAAE1B,aAAa;IACtE,OAAOoD;AACT;AAkBA,kEAAkE,GAClE,MAAMC,wBAAwB;AAE9B;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;CAgCC,GACD,OAAO,SAASC,iBACdJ,IAA8C;QAKxBA;IAHtB,IAAI,CAACA,MAAM,OAAOE;IAClB,IAAIF,KAAKK,sBAAsB,KAAK,MAAM,OAAOH;IACjD,IAAIF,KAAKM,sBAAsB,KAAK,MAAM,OAAOJ;IACjD,MAAMvC,SAASC,QAAOoC,cAAAA,KAAKC,KAAK,YAAVD,cAAc,IACjCnC,IAAI,GACJC,WAAW;IACd,IAAI,CAACqC,sBAAsBxB,IAAI,CAAChB,SAAS,OAAOuC;IAChD,IAAIvC,WAAWb,eAAea,OAAOO,QAAQ,CAAC,CAAC,CAAC,EAAEpB,aAAa,GAAG;QAChE,OAAOoD;IACT;IACA,OAAOvC;AACT"}
@@ -36,9 +36,18 @@ import { type AglynOrgCustomRole } from './org-permissions';
36
36
  * guessing one would let a request reach around the per-site toggle by
37
37
  * omitting the site.
38
38
  */
39
- /** The catalog keys a collaborator can hold per site, in catalog order. */
39
+ /**
40
+ * The catalog keys a collaborator can hold per site, in catalog order: every
41
+ * key with host-role defaults, the core's site keys (AGL-4047) and the ones
42
+ * plugins declare.
43
+ */
40
44
  export declare function hostPermissionKeys(): HostPermissionKey[];
41
- /** What a host role grants for every per-site key, before any toggle. */
45
+ /**
46
+ * What a host role grants a site collaborator for every per-site key, before
47
+ * any toggle. A key whose site-role preset holds for members of the whole
48
+ * workspace only (`workspaceMembersOnly`, AGL-4047) grants a collaborator
49
+ * nothing, whatever their site role.
50
+ */
42
51
  export declare function hostRolePermissionDefaults(role: HostAccessRole): Record<HostPermissionKey, boolean>;
43
52
  /**
44
53
  * A collaborator's verdict on ONE site for every per-site key, or null where
@@ -47,6 +56,114 @@ export declare function hostRolePermissionDefaults(role: HostAccessRole): Record
47
56
  * `hostPermissions` existed resolve exactly as it did before.
48
57
  */
49
58
  export declare function resolveCollaboratorHostPermissions(member: Partial<AglynOrgMember> | null | undefined, hostId: HostUid | null | undefined): Record<HostPermissionKey, boolean> | null;
59
+ /** The catalog keys whose preset is the member's role on the site, in catalog order. */
60
+ export declare function siteRolePresetPermissionKeys(): string[];
61
+ /** Is this a key whose preset is the member's role on the site? */
62
+ export declare function isSiteRolePresetPermission(permission: string): boolean;
63
+ /**
64
+ * A member's verdict on ONE site for every key whose preset is their role
65
+ * there (see above). `customRole` is their custom role document when they
66
+ * carry a `roleId` and the caller has it, and is ignored for a collaborator.
67
+ * No site named, or no access to it, refuses every such key.
68
+ */
69
+ export declare function resolveSiteRolePresetPermissions(member: (Partial<AglynOrgMember> & {
70
+ roleId?: string | null;
71
+ }) | null | undefined, hostId: HostUid | null | undefined, customRole?: Pick<AglynOrgCustomRole, 'permissions'> | null): Record<string, boolean>;
72
+ /**
73
+ * `granted` with every key whose preset is the site role decided for this
74
+ * site. With no site named it is `granted` as given, so a resolver can call
75
+ * it unconditionally.
76
+ */
77
+ export declare function applySiteRolePresets<T extends Record<string, boolean>>(granted: T, member: (Partial<AglynOrgMember> & {
78
+ roleId?: string | null;
79
+ }) | null | undefined, hostId: HostUid | null | undefined, customRole?: Pick<AglynOrgCustomRole, 'permissions'> | null): T;
80
+ /**
81
+ * The verdict the site's own stamp gives a member for one key whose preset
82
+ * is the site role, as the Firestore rules read it (AGL-4047): the site's
83
+ * `memberPermissions[uid][key]` when the projection wrote one, else the
84
+ * preset of the role the site's `memberRoles` projection holds for them.
85
+ *
86
+ * For a door that has the host document in hand and asks what the rules
87
+ * ask, so the two cannot disagree: the stamp is written beside
88
+ * `memberRoles` by the same projection, with every layer applied, and a
89
+ * site the projection has not reached since the key existed answers exactly
90
+ * as the role gate it replaced. `presetFor` is the declaring plugin's own
91
+ * table, so the answer does not depend on which plugins this process loaded.
92
+ */
93
+ export declare function hostStampedSitePermission(host: {
94
+ memberPermissions?: unknown;
95
+ memberRoles?: unknown;
96
+ } | null | undefined, uid: string, permission: string, presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>): boolean;
97
+ /**
98
+ * The same for a key whose preset holds for members of the whole workspace
99
+ * only (`workspaceMembersOnly`, AGL-4047), which `memberRoles` alone cannot
100
+ * answer: an admin of one site and an admin of the whole workspace project
101
+ * the same `'admin'` there. The site's stamp when the projection wrote one;
102
+ * else the preset of the role `memberRoles` holds for them AND of the role
103
+ * the fresh resolution gives them on the site, for a member of the whole
104
+ * workspace only. Exactly the gate these keys replaced: the projection's
105
+ * role, `orgWide` and the fresh site role, all three.
106
+ */
107
+ export declare function hostStampedWorkspaceSitePermission(host: {
108
+ memberPermissions?: unknown;
109
+ memberRoles?: unknown;
110
+ } | null | undefined, uid: string, permission: string, presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>, membership: {
111
+ orgWide: boolean;
112
+ hostRole: string | null | undefined;
113
+ }): boolean;
114
+ /** How one site narrows a workspace-wide member: the access they hold there, the site's verdicts, and which keys follow it. */
115
+ export interface SiteNarrowing {
116
+ /** Their role on the site, narrower than their workspace role. */
117
+ role: HostAccessRole;
118
+ /** `hostPermissions[hostId]`: the verdicts the site set for them. */
119
+ grants: Readonly<Partial<Record<string, unknown>>>;
120
+ /**
121
+ * Every key a site can narrow, `true` where it follows the site role for
122
+ * this member and `false` where they keep it (`siteNarrowingChoices`).
123
+ */
124
+ narrows: Readonly<Record<string, boolean>>;
125
+ }
126
+ /** The catalog keys a site CAN narrow (`narrowsPerSite`), in catalog order. */
127
+ export declare function siteNarrowedPermissionKeys(): string[];
128
+ /**
129
+ * Could any site narrow a member of this org role? Not the owner or an
130
+ * admin, who are admins of every site, and not a role whose site access has
131
+ * nothing narrower below it (a viewer). The editors' control is the one that
132
+ * changes anything; the role editor says so beside it.
133
+ */
134
+ export declare function roleCanBeNarrowed(role: string | null | undefined): boolean;
135
+ /**
136
+ * Whether each key a site can narrow follows the site role for this member
137
+ * (`true`) or is kept (`false`): their own choice, else their custom role's
138
+ * (when they carry its id and it was supplied), else the built-in role's
139
+ * default.
140
+ */
141
+ export declare function siteNarrowingChoices(member: (Partial<AglynOrgMember> & {
142
+ roleId?: string | null;
143
+ }) | null | undefined, customRole?: Pick<AglynOrgCustomRole, 'siteNarrowing'> | null): Record<string, boolean>;
144
+ /**
145
+ * Is this member's access to this site narrower than their workspace role?
146
+ * The cheap half of `siteNarrowingFor`, which needs no custom role: a caller
147
+ * that has to read one asks this first, and reads nothing for everyone else.
148
+ */
149
+ export declare function siteMayNarrow(member: Partial<AglynOrgMember> | null | undefined, hostId: HostUid | null | undefined): HostAccessRole | null;
150
+ /**
151
+ * How this site narrows this member, or null where it does not: a member
152
+ * who is not workspace-wide, has no access to the site, whose access there
153
+ * is as wide as their workspace role (owners and admins always), or who
154
+ * keeps every key a site can narrow. `customRole` is their custom role
155
+ * document when they carry a `roleId`; without it, only their own choices
156
+ * and the built-in defaults decide.
157
+ */
158
+ export declare function siteNarrowingFor(member: Partial<AglynOrgMember> | null | undefined, hostId: HostUid | null | undefined, customRole?: Pick<AglynOrgCustomRole, 'siteNarrowing'> | null): SiteNarrowing | null;
159
+ /** The keys that follow the site role under a narrowing, in catalog order; none without one. */
160
+ export declare function narrowedPermissionKeysOf(narrowing: SiteNarrowing | null | undefined): string[];
161
+ /**
162
+ * A workspace-wide member's resolved map with the keys that follow the site
163
+ * role decided for that site (see above). With no narrowing it is the map
164
+ * as given, so every resolver can call it unconditionally.
165
+ */
166
+ export declare function narrowPermissionsToSite<T extends Record<string, boolean>>(granted: T, narrowing: SiteNarrowing | null | undefined): T;
50
167
  /**
51
168
  * A member's verdict for every per-site key, on whichever axis their
52
169
  * membership is on.
@@ -55,7 +172,14 @@ export declare function resolveCollaboratorHostPermissions(member: Partial<Aglyn
55
172
  * `roleId` and the caller has it in hand, and is ignored for a collaborator,
56
173
  * whose org role is a seat-accounting fact rather than a grant. `hostId` is
57
174
  * the site the request is about; an org-wide member resolves the same on
58
- * every site and with none named.
175
+ * every site and with none named, except on a site that narrows them.
176
+ *
177
+ * An org-wide member's map also carries every key a site can narrow
178
+ * (AGL-4047), decided for `hostId`, so the site's `memberPermissions`
179
+ * projection answers the rules what the server answers. A collaborator's
180
+ * map does not: their register and waivers keys come from their site role
181
+ * and their own overrides, which the rules read from their member document
182
+ * as before.
59
183
  */
60
184
  export declare function resolveMemberHostPermissions(options: {
61
185
  member: Partial<AglynOrgMember> | null | undefined;
@@ -74,3 +198,35 @@ export declare function resolveMemberHostPermissions(options: {
74
198
  export declare function projectHostMemberPermissions(members: ReadonlyArray<Partial<AglynOrgMember> & {
75
199
  $id: string;
76
200
  }>, hostId: HostUid, customRoles: ReadonlyMap<string, AglynOrgCustomRole | null>): Record<string, Record<HostPermissionKey, boolean>>;
201
+ /** The core's site keys (AGL-4047): `CORE_SITE_ROLE_PRESETS`. */
202
+ export type CoreSiteKey = 'site.edit' | 'site.publish' | 'site.sensitive' | 'site.manage';
203
+ /** A host document, as a snapshot (`get`) or as its data. */
204
+ export type HostDocumentLike = {
205
+ get(field: string): unknown;
206
+ } | {
207
+ memberPermissions?: unknown;
208
+ memberRoles?: unknown;
209
+ } | null | undefined;
210
+ /**
211
+ * Does this member hold one of the core's site keys on this site (AGL-4047)?
212
+ * The door's twin of the rules' `canWriteHostContent`,
213
+ * `canPublishHostContent`, `canReadHostSensitive` and site-settings checks:
214
+ * the site's `memberPermissions` stamp, else the preset of the member's
215
+ * `memberRoles` entry (`hostStampedSitePermission`), so a route that asks it
216
+ * answers what the rules answer for the same write. For the routes that read
217
+ * the host document already; it costs no read.
218
+ */
219
+ export declare function holdsSiteKey(hostDoc: HostDocumentLike, uid: string, key: CoreSiteKey): boolean;
220
+ /**
221
+ * The same for a plugin's key whose preset is the site role (AGL-4047): the
222
+ * site's `memberPermissions` stamp, else `presetFor` read for the member's
223
+ * `memberRoles` entry. `presetFor` is the declaring plugin's own table, so the
224
+ * answer does not depend on which plugins this process loaded. For the doors
225
+ * and the cards that hold the host document already; it costs no read.
226
+ */
227
+ export declare function holdsStampedSiteKey(hostDoc: HostDocumentLike, uid: string | null | undefined, key: string, presetFor: Readonly<Partial<Record<HostAccessRole, boolean>>>): boolean;
228
+ /**
229
+ * The refusal a door answers when a member does not hold a site key
230
+ * (AGL-4047): it names the permission and who can change it, never a role.
231
+ */
232
+ export declare function siteKeyRefusal(key: string): string;