@aglyn/plugins-ai 1.0.0-beta.278 → 1.0.0-beta.283

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 (76) hide show
  1. package/package.json +17 -17
  2. package/src/lib/activity/ai-activity-actions.d.ts +2 -26
  3. package/src/lib/activity/ai-activity-actions.js +5 -42
  4. package/src/lib/activity/ai-activity-actions.js.map +1 -1
  5. package/src/lib/activity/ai-activity-words.d.ts +42 -0
  6. package/src/lib/activity/ai-activity-words.js +63 -0
  7. package/src/lib/activity/ai-activity-words.js.map +1 -0
  8. package/src/lib/components/ai-insight-card.component.d.ts +2 -6
  9. package/src/lib/components/ai-insight-card.component.js +2 -6
  10. package/src/lib/components/ai-insight-card.component.js.map +1 -1
  11. package/src/lib/components/ai-insight-dialog.component.js +22 -32
  12. package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
  13. package/src/lib/components/ai-job-links.js +15 -0
  14. package/src/lib/components/ai-job-links.js.map +1 -1
  15. package/src/lib/components/assist-panel.component.js +3 -1
  16. package/src/lib/components/assist-panel.component.js.map +1 -1
  17. package/src/lib/jobs/ai-build-capabilities.js +23 -0
  18. package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
  19. package/src/lib/jobs/ai-job-build-step.js +2 -1
  20. package/src/lib/jobs/ai-job-build-step.js.map +1 -1
  21. package/src/lib/jobs/ai-job-import-step.d.ts +1 -0
  22. package/src/lib/jobs/ai-job-import-step.js +17 -11
  23. package/src/lib/jobs/ai-job-import-step.js.map +1 -1
  24. package/src/lib/jobs/ai-job-plan-step.js +3 -2
  25. package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
  26. package/src/lib/jobs/ai-job-privacy-step.d.ts +184 -0
  27. package/src/lib/jobs/ai-job-privacy-step.js +471 -0
  28. package/src/lib/jobs/ai-job-privacy-step.js.map +1 -0
  29. package/src/lib/jobs/ai-site-inspiration.d.ts +10 -0
  30. package/src/lib/jobs/ai-site-inspiration.js +15 -0
  31. package/src/lib/jobs/ai-site-inspiration.js.map +1 -1
  32. package/src/lib/model/ai-insight-ask.d.ts +57 -0
  33. package/src/lib/model/ai-insight-ask.js +64 -0
  34. package/src/lib/model/ai-insight-ask.js.map +1 -0
  35. package/src/lib/model/ai-insight.d.ts +1 -6
  36. package/src/lib/model/ai-insight.js +2 -8
  37. package/src/lib/model/ai-insight.js.map +1 -1
  38. package/src/lib/model/ai-job-activity.js +1 -0
  39. package/src/lib/model/ai-job-activity.js.map +1 -1
  40. package/src/lib/model/ai-jobs.types.d.ts +5 -1
  41. package/src/lib/model/ai-jobs.types.js +1 -0
  42. package/src/lib/model/ai-jobs.types.js.map +1 -1
  43. package/src/lib/model/ai-permissions.js +6 -2
  44. package/src/lib/model/ai-permissions.js.map +1 -1
  45. package/src/lib/model/ai-site-import.d.ts +2 -0
  46. package/src/lib/model/ai-site-import.js +2 -0
  47. package/src/lib/model/ai-site-import.js.map +1 -1
  48. package/src/lib/model/ai-site-kinds.d.ts +10 -28
  49. package/src/lib/model/ai-site-kinds.js +12 -496
  50. package/src/lib/model/ai-site-kinds.js.map +1 -1
  51. package/src/lib/providers/anthropic.js +23 -14
  52. package/src/lib/providers/anthropic.js.map +1 -1
  53. package/src/lib/providers/contract.d.ts +39 -4
  54. package/src/lib/providers/contract.js +28 -0
  55. package/src/lib/providers/contract.js.map +1 -1
  56. package/src/lib/providers/routing.d.ts +3 -0
  57. package/src/lib/providers/routing.js +4 -3
  58. package/src/lib/providers/routing.js.map +1 -1
  59. package/src/lib/runtime/ai-dev-env.d.ts +3 -2
  60. package/src/lib/runtime/ai-dev-env.js +3 -2
  61. package/src/lib/runtime/ai-dev-env.js.map +1 -1
  62. package/src/lib/runtime/ai-page-links.d.ts +5 -2
  63. package/src/lib/runtime/ai-page-links.js +11 -3
  64. package/src/lib/runtime/ai-page-links.js.map +1 -1
  65. package/src/lib/runtime/ai-runtime.js +5 -2
  66. package/src/lib/runtime/ai-runtime.js.map +1 -1
  67. package/src/lib/server/assist-chat.js +58 -30
  68. package/src/lib/server/assist-chat.js.map +1 -1
  69. package/src/lib/server.js +2 -0
  70. package/src/lib/server.js.map +1 -1
  71. package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +3 -1
  72. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +19 -1
  73. package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -1
  74. package/src/lib/site-import/site-import-page.d.ts +2 -2
  75. package/src/lib/site-import/site-import-page.js +11 -1
  76. package/src/lib/site-import/site-import-page.js.map +1 -1
@@ -0,0 +1,184 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2026 Aglyn LLC
4
+ *
5
+ * Licensed under the Apache License, Version 2.0 (the "License");
6
+ * you may not use this file except in compliance with the License.
7
+ * You may obtain a copy of the License at
8
+ *
9
+ * http://www.apache.org/licenses/LICENSE-2.0
10
+ *
11
+ * Unless required by applicable law or agreed to in writing, software
12
+ * distributed under the License is distributed on an "AS IS" BASIS,
13
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
+ * See the License for the specific language governing permissions and
15
+ * limitations under the License.
16
+ */
17
+ import { dropPluginSiteCache } from '@aglyn/aglyn/plugin-manager/plugin-site-cache';
18
+ import type { PluginAiCapabilityArgsSchema } from '@aglyn/aglyn/plugin-manager/plugin-ai-capabilities';
19
+ import { getLockdownVerdict } from '@aglyn/tenant-data-admin/server/lockdown';
20
+ import { resolveOrgIdForHost } from '@aglyn/tenant-data-admin/server/organizations';
21
+ import { type Firestore } from 'firebase-admin/firestore';
22
+ import { type AiJobAdmission } from './ai-job-admission';
23
+ import type { AiJobStepRunner } from './ai-job-text-step';
24
+ /**
25
+ * The `privacy` step (AGL-4033): who can see the site, as the console's own
26
+ * three controls set it — maintenance mode (Admin → Error pages), the
27
+ * Discourage search engines switch (Setup → SEO) and a page's Visibility
28
+ * (its Page Access card) — as a first-class build operation, so "make my site
29
+ * private until launch" or "hide the pricing page" is one item on Assist's
30
+ * plan card. The docs page it mirrors is Site Protection → Make your site
31
+ * private.
32
+ *
33
+ * NO MODEL. The planner already wrote the choices as the item's arguments;
34
+ * this checks each against the rule the console's own control holds it to and
35
+ * writes it, so it costs no credits.
36
+ *
37
+ * ── Only the controls' own fields ────────────────────────────────────────
38
+ *
39
+ * `maintenance` (true, or deleted on the way off, as the Error pages card
40
+ * writes it), `seo.discourageSearchEngines` (likewise, as the Search engines
41
+ * card does), and a page's `visibility` as Public, Unlisted or Members only.
42
+ * Leaving password mode drops the page's stored hash, as the Page Access card
43
+ * does, so protection never lingers on a page that no longer asks for it.
44
+ *
45
+ * NEVER: a page password. Setting one would put the password in the chat and
46
+ * the plan, so it is not an argument; the note sends the person to the Page
47
+ * Access card instead. Nor layouts, emails or record templates, which have no
48
+ * Page Access card, nor the error pages themselves.
49
+ *
50
+ * ── Who may ──────────────────────────────────────────────────────────────
51
+ *
52
+ * Search indexing and page visibility: the Firestore rules' own for those
53
+ * keys, a member with a write role on the site (admin, editor or author)
54
+ * while no lockdown freezes it. Maintenance mode takes every page of the site
55
+ * offline for every visitor, so, as the console keeps its card in the Admin
56
+ * area, only a site ADMIN may change it here. A non-admin's request still
57
+ * makes the rest of its changes and says why that one was not made.
58
+ *
59
+ * Members only is made only where the plan has content gating (Business and
60
+ * above): the tenant serves gated pages nowhere else, so on a lower plan the
61
+ * write would close the page to everyone, and the note says why it was not.
62
+ *
63
+ * ── Live on confirm, idempotent ──────────────────────────────────────────
64
+ *
65
+ * As the `settings` and `tracking` steps: none of these has a draft, so the
66
+ * plan card's confirm is the agreement and the write is live. One transaction
67
+ * re-reads the host and each page, re-checks the role, writes only what
68
+ * differs, and queues a site-wide publish outbox entry, because visibility
69
+ * changes navigation and the sitemap and both switches change every page.
70
+ */
71
+ export declare const AI_PRIVACY_KIND: "privacy";
72
+ /** ASSUMED: the least time the step needs — the site, the org, the pages named and one transaction. */
73
+ export declare const AI_JOB_PRIVACY_STEP_MINIMUM_MS = 5000;
74
+ export declare const AI_PRIVACY_NO_SITE_COPY = "Open the site whose privacy should change.";
75
+ export declare const AI_PRIVACY_ROLE_COPY = "Changing who can see the site needs the admin, editor or author role on this site.";
76
+ export declare const AI_PRIVACY_ADMIN_COPY = "Only a site admin can turn maintenance mode on or off, so it was left as it is.";
77
+ export declare const AI_PRIVACY_LOCKED_COPY = "This site is not accepting changes right now.";
78
+ export declare const AI_PRIVACY_NOTHING_COPY = "The request named no privacy change this could make.";
79
+ export declare const AI_PRIVACY_PASSWORD_COPY: string;
80
+ /** The most pages one item may change. */
81
+ export declare const AI_PRIVACY_MAX_PAGES = 20;
82
+ /** The visibilities a build may set, by the Page Access card's labels. */
83
+ export declare const AI_PRIVACY_VISIBILITIES: readonly ["public", "unlisted", "members"];
84
+ export type AiPrivacyVisibility = (typeof AI_PRIVACY_VISIBILITIES)[number];
85
+ /** Where a person finds each change, as the output links and notes name it. */
86
+ export declare const AI_PRIVACY_OUTPUTS: {
87
+ readonly maintenance: {
88
+ readonly id: "maintenance";
89
+ readonly label: "Maintenance mode";
90
+ readonly where: "Admin → Error pages";
91
+ };
92
+ readonly search: {
93
+ readonly id: "search";
94
+ readonly label: "Search engines";
95
+ readonly where: "Setup → SEO";
96
+ };
97
+ readonly pages: {
98
+ readonly id: "pages";
99
+ readonly label: "Page access";
100
+ readonly where: "each page’s Page Access card";
101
+ };
102
+ };
103
+ /** Where the operation's changes are made, as the build's notes name it. */
104
+ export declare const AI_PRIVACY_WHERE = "the site\u2019s privacy settings";
105
+ /** The item's arguments, as the planner fills them in. Every one optional. */
106
+ export declare const AI_PRIVACY_ARGS_SCHEMA: PluginAiCapabilityArgsSchema;
107
+ /** What an item asks: each switch to set, and each page's new visibility. */
108
+ export interface AiPrivacyRequest {
109
+ maintenance: boolean | null;
110
+ discourageSearchEngines: boolean | null;
111
+ pages: {
112
+ page: string;
113
+ visibility: AiPrivacyVisibility;
114
+ }[];
115
+ }
116
+ export interface AiPrivacyRead {
117
+ request: AiPrivacyRequest;
118
+ /** A sentence per value that could not be used, so the rest still are. */
119
+ problems: string[];
120
+ }
121
+ /** The item's arguments read: what may be written, and a sentence for each that may not. */
122
+ export declare function aiPrivacyRequestOf(inputs: Readonly<Record<string, unknown>> | null | undefined): AiPrivacyRead;
123
+ /** Whether a read request asks for anything at all. */
124
+ export declare function aiPrivacyRequestEmpty(request: AiPrivacyRequest): boolean;
125
+ export type AiPrivacyWrite = {
126
+ ok: true;
127
+ /** A sentence per change asked, made now or already so, by output id. */
128
+ done: {
129
+ output: keyof typeof AI_PRIVACY_OUTPUTS;
130
+ said: string;
131
+ }[];
132
+ /** A sentence per part that was not made, and why. */
133
+ notDone: string[];
134
+ changedAny: boolean;
135
+ outboxId: string | null;
136
+ } | {
137
+ ok: false;
138
+ error: string;
139
+ };
140
+ /**
141
+ * The changes written, in one transaction that reads the host and each named
142
+ * page as they are stored NOW: the member still holds a write role (and is an
143
+ * admin, for maintenance), each page exists and is a page, and only what
144
+ * differs from what is stored is written, with a site-wide publish outbox
145
+ * entry beside it so the live site's cached pages are dropped.
146
+ */
147
+ export declare function writeAiSitePrivacy(firestore: Firestore, input: {
148
+ hostId: string;
149
+ uid: string;
150
+ request: AiPrivacyRequest;
151
+ contentGating: boolean;
152
+ now: Date;
153
+ }): Promise<AiPrivacyWrite>;
154
+ /** Why this member may not change who can see this site now, or `null`. */
155
+ export declare function aiPrivacyAccessRefusal(input: {
156
+ host: FirebaseFirestore.DocumentSnapshot;
157
+ uid: string;
158
+ org: object | null;
159
+ request: AiPrivacyRequest;
160
+ }, deps?: {
161
+ lockdown?: typeof getLockdownVerdict;
162
+ }): Promise<string | null>;
163
+ /** One output's note: what changed, that it is live, and what was not done. */
164
+ export declare function aiPrivacyOutputNote(said: readonly string[], where: string, notDone: readonly string[], changedAny: boolean): string;
165
+ export interface AiJobPrivacyStepDeps {
166
+ write?: typeof writeAiSitePrivacy;
167
+ lockdown?: typeof getLockdownVerdict;
168
+ dropCache?: typeof dropPluginSiteCache;
169
+ }
170
+ export declare function createAiJobPrivacyStep(deps?: AiJobPrivacyStepDeps): AiJobStepRunner;
171
+ export declare const runAiJobPrivacyStep: AiJobStepRunner;
172
+ export interface AiPrivacyJobAdmissionDeps {
173
+ ownerOf?: typeof resolveOrgIdForHost;
174
+ lockdown?: typeof getLockdownVerdict;
175
+ }
176
+ /**
177
+ * A privacy change is admitted on a site of the job's own org, for a member
178
+ * who may write it now, where the request names at least one change this can
179
+ * make. Refused in those words before anything is written.
180
+ */
181
+ export declare function createAiPrivacyJobAdmission(deps?: AiPrivacyJobAdmissionDeps): AiJobAdmission;
182
+ export declare const aiPrivacyJobAdmission: AiJobAdmission;
183
+ /** Registers the privacy step and its door (AGL-4033). */
184
+ export declare function registerAiPrivacyJob(): void;
@@ -0,0 +1,471 @@
1
+ import { _ as _extends } from "@swc/helpers/_/_extends";
2
+ /**
3
+ * @license
4
+ * Copyright 2026 Aglyn LLC
5
+ *
6
+ * Licensed under the Apache License, Version 2.0 (the "License");
7
+ * you may not use this file except in compliance with the License.
8
+ * You may obtain a copy of the License at
9
+ *
10
+ * http://www.apache.org/licenses/LICENSE-2.0
11
+ *
12
+ * Unless required by applicable law or agreed to in writing, software
13
+ * distributed under the License is distributed on an "AS IS" BASIS,
14
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15
+ * See the License for the specific language governing permissions and
16
+ * limitations under the License.
17
+ */ import { createResourceUid } from "@aglyn/aglyn/app-utils/create-resource-uid";
18
+ import { hostRoleCanWrite } from "@aglyn/aglyn/app-utils/organizations";
19
+ import { aiAddonName, checkEntitlement } from "@aglyn/aglyn/app-utils/plan-entitlements";
20
+ import { PLATFORM_BRAND_NAME } from "@aglyn/aglyn/app-utils/platform-brand";
21
+ import { SCREEN_ROOT_PATH } from "@aglyn/aglyn/app-utils/screen-route";
22
+ import { HostScreenVisibility } from "@aglyn/aglyn/foundation/definitions/platform.types";
23
+ import { dropPluginSiteCache } from "@aglyn/aglyn/plugin-manager/plugin-site-cache";
24
+ import { getLockdownVerdict } from "@aglyn/tenant-data-admin/server/lockdown";
25
+ import { resolveOrgIdForHost } from "@aglyn/tenant-data-admin/server/organizations";
26
+ import { FieldValue } from "firebase-admin/firestore";
27
+ import { registerAiJobAdmission } from "./ai-job-admission.js";
28
+ import { aiJobEditModel } from "./ai-job-edit-step.js";
29
+ import { aiLimitReview, aiUnspentOutcome } from "./ai-job-generation.js";
30
+ import { registerAiJobStep } from "./ai-jobs.js";
31
+ /**
32
+ * The `privacy` step (AGL-4033): who can see the site, as the console's own
33
+ * three controls set it — maintenance mode (Admin → Error pages), the
34
+ * Discourage search engines switch (Setup → SEO) and a page's Visibility
35
+ * (its Page Access card) — as a first-class build operation, so "make my site
36
+ * private until launch" or "hide the pricing page" is one item on Assist's
37
+ * plan card. The docs page it mirrors is Site Protection → Make your site
38
+ * private.
39
+ *
40
+ * NO MODEL. The planner already wrote the choices as the item's arguments;
41
+ * this checks each against the rule the console's own control holds it to and
42
+ * writes it, so it costs no credits.
43
+ *
44
+ * ── Only the controls' own fields ────────────────────────────────────────
45
+ *
46
+ * `maintenance` (true, or deleted on the way off, as the Error pages card
47
+ * writes it), `seo.discourageSearchEngines` (likewise, as the Search engines
48
+ * card does), and a page's `visibility` as Public, Unlisted or Members only.
49
+ * Leaving password mode drops the page's stored hash, as the Page Access card
50
+ * does, so protection never lingers on a page that no longer asks for it.
51
+ *
52
+ * NEVER: a page password. Setting one would put the password in the chat and
53
+ * the plan, so it is not an argument; the note sends the person to the Page
54
+ * Access card instead. Nor layouts, emails or record templates, which have no
55
+ * Page Access card, nor the error pages themselves.
56
+ *
57
+ * ── Who may ──────────────────────────────────────────────────────────────
58
+ *
59
+ * Search indexing and page visibility: the Firestore rules' own for those
60
+ * keys, a member with a write role on the site (admin, editor or author)
61
+ * while no lockdown freezes it. Maintenance mode takes every page of the site
62
+ * offline for every visitor, so, as the console keeps its card in the Admin
63
+ * area, only a site ADMIN may change it here. A non-admin's request still
64
+ * makes the rest of its changes and says why that one was not made.
65
+ *
66
+ * Members only is made only where the plan has content gating (Business and
67
+ * above): the tenant serves gated pages nowhere else, so on a lower plan the
68
+ * write would close the page to everyone, and the note says why it was not.
69
+ *
70
+ * ── Live on confirm, idempotent ──────────────────────────────────────────
71
+ *
72
+ * As the `settings` and `tracking` steps: none of these has a draft, so the
73
+ * plan card's confirm is the agreement and the write is live. One transaction
74
+ * re-reads the host and each page, re-checks the role, writes only what
75
+ * differs, and queues a site-wide publish outbox entry, because visibility
76
+ * changes navigation and the sitemap and both switches change every page.
77
+ */ export const AI_PRIVACY_KIND = 'privacy';
78
+ /** ASSUMED: the least time the step needs — the site, the org, the pages named and one transaction. */ export const AI_JOB_PRIVACY_STEP_MINIMUM_MS = 5000;
79
+ export const AI_PRIVACY_NO_SITE_COPY = 'Open the site whose privacy should change.';
80
+ export const AI_PRIVACY_ROLE_COPY = 'Changing who can see the site needs the admin, editor or author role on this site.';
81
+ export const AI_PRIVACY_ADMIN_COPY = 'Only a site admin can turn maintenance mode on or off, so it was left as it is.';
82
+ export const AI_PRIVACY_LOCKED_COPY = 'This site is not accepting changes right now.';
83
+ export const AI_PRIVACY_NOTHING_COPY = 'The request named no privacy change this could make.';
84
+ export const AI_PRIVACY_PASSWORD_COPY = `To put a page behind a password, set it in that page’s Page Access card; ${aiAddonName()} never handles passwords.`;
85
+ /** The most pages one item may change. */ export const AI_PRIVACY_MAX_PAGES = 20;
86
+ /** The visibilities a build may set, by the Page Access card's labels. */ export const AI_PRIVACY_VISIBILITIES = [
87
+ 'public',
88
+ 'unlisted',
89
+ 'members'
90
+ ];
91
+ const VISIBILITY_VALUES = {
92
+ public: HostScreenVisibility.PUBLIC,
93
+ unlisted: HostScreenVisibility.UNLISTED,
94
+ members: HostScreenVisibility.AUTHENTICATED
95
+ };
96
+ const VISIBILITY_LABELS = {
97
+ public: 'Public',
98
+ unlisted: 'Unlisted',
99
+ members: 'Members only'
100
+ };
101
+ /** Where a person finds each change, as the output links and notes name it. */ export const AI_PRIVACY_OUTPUTS = {
102
+ maintenance: {
103
+ id: 'maintenance',
104
+ label: 'Maintenance mode',
105
+ where: 'Admin → Error pages'
106
+ },
107
+ search: {
108
+ id: 'search',
109
+ label: 'Search engines',
110
+ where: 'Setup → SEO'
111
+ },
112
+ pages: {
113
+ id: 'pages',
114
+ label: 'Page access',
115
+ where: 'each page’s Page Access card'
116
+ }
117
+ };
118
+ /** Where the operation's changes are made, as the build's notes name it. */ export const AI_PRIVACY_WHERE = 'the site’s privacy settings';
119
+ /** The item's arguments, as the planner fills them in. Every one optional. */ export const AI_PRIVACY_ARGS_SCHEMA = {
120
+ type: 'object',
121
+ properties: {
122
+ maintenance: {
123
+ type: 'boolean',
124
+ description: 'true takes the whole site offline: every address shows its 503 page to every visitor, including anyone with a link, until it is turned off. false brings the site back. Leave it out unless the person asked to take the whole site offline, keep it from everyone until launch, or bring it back.'
125
+ },
126
+ discourageSearchEngines: {
127
+ type: 'boolean',
128
+ description: 'true asks search engines not to index any page of the site (it stays open to anyone with a link); false lets them index it again. Leave it out unless the person asked about search engines or Google.'
129
+ },
130
+ publicPages: {
131
+ type: 'array',
132
+ description: 'Pages to make Public: anyone can open them, and they are offered to search engines. Each by its id from the site inventory.',
133
+ items: {
134
+ type: 'string',
135
+ maxLength: 64
136
+ },
137
+ maxItems: AI_PRIVACY_MAX_PAGES
138
+ },
139
+ unlistedPages: {
140
+ type: 'array',
141
+ description: 'Pages to make Unlisted: anyone with the link can open them, kept out of search results, the sitemap and navigation. Each by its id from the site inventory.',
142
+ items: {
143
+ type: 'string',
144
+ maxLength: 64
145
+ },
146
+ maxItems: AI_PRIVACY_MAX_PAGES
147
+ },
148
+ membersPages: {
149
+ type: 'array',
150
+ description: 'Pages to make Members only: only visitors signed in to the site can open them (Business plan and above). Each by its id from the site inventory.',
151
+ items: {
152
+ type: 'string',
153
+ maxLength: 64
154
+ },
155
+ maxItems: AI_PRIVACY_MAX_PAGES
156
+ }
157
+ },
158
+ additionalProperties: false
159
+ };
160
+ /** Each visibility's argument, as the planner lists the pages for it. */ const VISIBILITY_ARGS = {
161
+ public: 'publicPages',
162
+ unlisted: 'unlistedPages',
163
+ members: 'membersPages'
164
+ };
165
+ const flag = (value)=>typeof value === 'boolean' ? value : null;
166
+ /** A page id as the site inventory gives it: one document id, never a path. */ const PAGE_ID = /^[A-Za-z0-9_-]{1,64}$/;
167
+ /** The item's arguments read: what may be written, and a sentence for each that may not. */ export function aiPrivacyRequestOf(inputs) {
168
+ const problems = [];
169
+ const pages = [];
170
+ const asked = new Map();
171
+ for (const visibility of AI_PRIVACY_VISIBILITIES){
172
+ const listed = inputs == null ? void 0 : inputs[VISIBILITY_ARGS[visibility]];
173
+ for (const entry of Array.isArray(listed) ? listed : []){
174
+ var _asked_get;
175
+ const page = typeof entry === 'string' ? entry.trim() : '';
176
+ if (PAGE_ID.test(page)) asked.set(page, ((_asked_get = asked.get(page)) != null ? _asked_get : new Set()).add(visibility));
177
+ }
178
+ }
179
+ for (const [page, visibilities] of asked){
180
+ // A page asked to be two things is left alone rather than guessed at.
181
+ if (visibilities.size > 1) problems.push('A page was asked to be two things at once, so it was left as it is.');
182
+ else pages.push({
183
+ page,
184
+ visibility: [
185
+ ...visibilities
186
+ ][0]
187
+ });
188
+ }
189
+ if (pages.length > AI_PRIVACY_MAX_PAGES) {
190
+ pages.splice(AI_PRIVACY_MAX_PAGES);
191
+ problems.push(`Only the first ${AI_PRIVACY_MAX_PAGES} pages were changed; ask again for the rest.`);
192
+ }
193
+ return {
194
+ request: {
195
+ maintenance: flag(inputs == null ? void 0 : inputs['maintenance']),
196
+ discourageSearchEngines: flag(inputs == null ? void 0 : inputs['discourageSearchEngines']),
197
+ pages
198
+ },
199
+ problems
200
+ };
201
+ }
202
+ /** Whether a read request asks for anything at all. */ export function aiPrivacyRequestEmpty(request) {
203
+ return request.maintenance === null && request.discourageSearchEngines === null && request.pages.length === 0;
204
+ }
205
+ /**
206
+ * The changes written, in one transaction that reads the host and each named
207
+ * page as they are stored NOW: the member still holds a write role (and is an
208
+ * admin, for maintenance), each page exists and is a page, and only what
209
+ * differs from what is stored is written, with a site-wide publish outbox
210
+ * entry beside it so the live site's cached pages are dropped.
211
+ */ export async function writeAiSitePrivacy(firestore, input) {
212
+ const hostRef = firestore.collection('hosts').doc(input.hostId);
213
+ const screens = hostRef.collection('screens');
214
+ return firestore.runTransaction(async (tx)=>{
215
+ var _host_get;
216
+ const host = await tx.get(hostRef);
217
+ if (!host.exists) return {
218
+ ok: false,
219
+ error: 'Unknown site'
220
+ };
221
+ const role = ((_host_get = host.get('memberRoles')) != null ? _host_get : {})[input.uid];
222
+ if (!hostRoleCanWrite(role)) return {
223
+ ok: false,
224
+ error: AI_PRIVACY_ROLE_COPY
225
+ };
226
+ const pageDocs = await Promise.all(input.request.pages.map((one)=>tx.get(screens.doc(one.page))));
227
+ const done = [];
228
+ const notDone = [];
229
+ const hostUpdates = {};
230
+ const pageUpdates = [];
231
+ const { maintenance, discourageSearchEngines } = input.request;
232
+ if (maintenance !== null) {
233
+ if (role !== 'admin') {
234
+ notDone.push(AI_PRIVACY_ADMIN_COPY);
235
+ } else {
236
+ var _host_get1;
237
+ if (host.get('maintenance') === true !== maintenance) {
238
+ hostUpdates['maintenance'] = maintenance || FieldValue.delete();
239
+ }
240
+ const assigned = !!((_host_get1 = host.get('errorScreens')) == null ? void 0 : _host_get1['unavailable']);
241
+ done.push({
242
+ output: 'maintenance',
243
+ said: maintenance ? `Maintenance mode is on: every address shows ${assigned ? 'your 503 page' : `${PLATFORM_BRAND_NAME}’s plain “undergoing maintenance” notice, because no 503 page is assigned yet`} to every visitor.` : 'Maintenance mode is off: the site is open to visitors again.'
244
+ });
245
+ }
246
+ }
247
+ if (discourageSearchEngines !== null) {
248
+ var _host_get2;
249
+ const stored = ((_host_get2 = host.get('seo')) == null ? void 0 : _host_get2['discourageSearchEngines']) === true;
250
+ if (stored !== discourageSearchEngines) {
251
+ hostUpdates['seo.discourageSearchEngines'] = discourageSearchEngines || FieldValue.delete();
252
+ }
253
+ done.push({
254
+ output: 'search',
255
+ said: discourageSearchEngines ? 'Search engines are asked not to index this site. Anyone with a link can still open it.' : 'Search engines may index this site again. They take days to come back.'
256
+ });
257
+ }
258
+ input.request.pages.forEach((asked, index)=>{
259
+ var _page_get, _page_get1;
260
+ var _page_get2;
261
+ const page = pageDocs[index];
262
+ if (!page.exists || page.get('deletedAt') != null || page.get('kind')) {
263
+ notDone.push('A page named is not one of this site’s pages, so it was left out.');
264
+ return;
265
+ }
266
+ const name = String((_page_get = page.get('displayName')) != null ? _page_get : '').trim() || 'A page';
267
+ if (asked.visibility === 'members' && !input.contentGating) {
268
+ // The tenant serves gated pages only on plans with content gating, so
269
+ // writing it here would close the page to everyone.
270
+ notDone.push(`“${name}” was not made Members only: members-only pages need the Business plan or above.`);
271
+ return;
272
+ }
273
+ const value = VISIBILITY_VALUES[asked.visibility];
274
+ const update = {};
275
+ if (((_page_get1 = page.get('visibility')) != null ? _page_get1 : HostScreenVisibility.PUBLIC) !== value) update['visibility'] = value;
276
+ const hadPassword = !!((_page_get2 = page.get('protection')) == null ? void 0 : _page_get2['passwordHash']);
277
+ if (hadPassword) update['protection'] = FieldValue.delete();
278
+ if (Object.keys(update).length) pageUpdates.push({
279
+ ref: page.ref,
280
+ update: _extends({}, update, {
281
+ updatedAt: input.now
282
+ })
283
+ });
284
+ const extra = hadPassword ? ' Its page password was removed.' : '';
285
+ done.push({
286
+ output: 'pages',
287
+ said: `“${name}” is now ${VISIBILITY_LABELS[asked.visibility]}.${extra}`
288
+ });
289
+ });
290
+ const changedAny = Object.keys(hostUpdates).length > 0 || pageUpdates.length > 0;
291
+ if (!changedAny) return {
292
+ ok: true,
293
+ done,
294
+ notDone,
295
+ changedAny,
296
+ outboxId: null
297
+ };
298
+ if (Object.keys(hostUpdates).length) tx.update(hostRef, _extends({}, hostUpdates, {
299
+ updatedAt: input.now
300
+ }));
301
+ for (const { ref, update } of pageUpdates)tx.update(ref, update);
302
+ // Both switches change every page, and a page's visibility changes the
303
+ // navigation and the sitemap every other page carries.
304
+ const outboxRef = firestore.collection('publishOutbox').doc(createResourceUid());
305
+ tx.set(outboxRef, {
306
+ hostId: input.hostId,
307
+ paths: [
308
+ SCREEN_ROOT_PATH
309
+ ],
310
+ entireHost: true,
311
+ createdAt: FieldValue.serverTimestamp(),
312
+ attempts: 0
313
+ });
314
+ return {
315
+ ok: true,
316
+ done,
317
+ notDone,
318
+ changedAny,
319
+ outboxId: outboxRef.id
320
+ };
321
+ });
322
+ }
323
+ // ── The step ──────────────────────────────────────────────────────────────
324
+ /** Why this member may not change who can see this site now, or `null`. */ export async function aiPrivacyAccessRefusal(input, deps = {}) {
325
+ var _input_host_get, _deps_lockdown;
326
+ if (!input.host.exists) return 'Unknown site';
327
+ const role = ((_input_host_get = input.host.get('memberRoles')) != null ? _input_host_get : {})[input.uid];
328
+ if (!hostRoleCanWrite(role)) return AI_PRIVACY_ROLE_COPY;
329
+ // A request that is ONLY maintenance has nothing a non-admin may make.
330
+ const onlyMaintenance = input.request.maintenance !== null && input.request.discourageSearchEngines === null && !input.request.pages.length;
331
+ if (onlyMaintenance && role !== 'admin') return AI_PRIVACY_ADMIN_COPY;
332
+ const locked = await ((_deps_lockdown = deps.lockdown) != null ? _deps_lockdown : getLockdownVerdict)({
333
+ staff: false,
334
+ uid: input.uid,
335
+ org: input.org,
336
+ host: input.host.data(),
337
+ intent: 'write'
338
+ });
339
+ return locked ? AI_PRIVACY_LOCKED_COPY : null;
340
+ }
341
+ /** One output's note: what changed, that it is live, and what was not done. */ export function aiPrivacyOutputNote(said, where, notDone, changedAny) {
342
+ const did = said.length ? `${said.join(' ')} Live now, in ${where}.${changedAny ? '' : ' It already was, so nothing was written.'}` : 'Nothing needed changing.';
343
+ const not = notDone.length ? ` Not done: ${notDone.join(' ')}` : '';
344
+ return `${did}${not}`.slice(0, 1000);
345
+ }
346
+ export function createAiJobPrivacyStep(deps = {}) {
347
+ var _deps_write, _deps_dropCache;
348
+ const write = (_deps_write = deps.write) != null ? _deps_write : writeAiSitePrivacy;
349
+ const dropCache = (_deps_dropCache = deps.dropCache) != null ? _deps_dropCache : dropPluginSiteCache;
350
+ return async ({ job, now, firestore, org, modelFor })=>{
351
+ var _ref, _data;
352
+ // No model is asked: the outcome records the edit step's at no cost.
353
+ const model = (_ref = modelFor == null ? void 0 : modelFor('job.edit')) != null ? _ref : aiJobEditModel();
354
+ const unspent = (failure)=>_extends({}, aiUnspentOutcome(model), {
355
+ failure
356
+ });
357
+ const hostId = job.hostId;
358
+ if (!hostId) return unspent(AI_PRIVACY_NO_SITE_COPY);
359
+ const read = aiPrivacyRequestOf(job.inputs);
360
+ if (aiPrivacyRequestEmpty(read.request)) return unspent([
361
+ AI_PRIVACY_NOTHING_COPY,
362
+ ...read.problems
363
+ ].join(' '));
364
+ const orgData = org != null ? org : (_data = (await firestore.collection('orgs').doc(job.orgId).get()).data()) != null ? _data : null;
365
+ const host = await firestore.collection('hosts').doc(hostId).get();
366
+ const refused = await aiPrivacyAccessRefusal({
367
+ host,
368
+ uid: job.createdBy,
369
+ org: orgData,
370
+ request: read.request
371
+ }, {
372
+ lockdown: deps.lockdown
373
+ });
374
+ if (refused) return aiUnspentOutcome(model, {
375
+ review: aiLimitReview(refused)
376
+ });
377
+ const written = await write(firestore, {
378
+ hostId,
379
+ uid: job.createdBy,
380
+ request: read.request,
381
+ contentGating: checkEntitlement(orgData, 'contentGating'),
382
+ now
383
+ });
384
+ if (written.ok === false) return unspent(written.error);
385
+ if (written.outboxId) {
386
+ // The live site's cached pages go now; the outbox entry stays for the
387
+ // drain unless the drop certainly happened.
388
+ const dropped = await dropCache({
389
+ hostIds: [
390
+ hostId
391
+ ],
392
+ reason: 'AI build changed site privacy'
393
+ }).catch(()=>({
394
+ complete: false
395
+ }));
396
+ if (dropped.complete) await firestore.collection('publishOutbox').doc(written.outboxId).delete().catch(()=>undefined);
397
+ }
398
+ const notDone = [
399
+ ...read.problems,
400
+ ...written.notDone
401
+ ];
402
+ const hostSubdomain = typeof host.get('subdomain') === 'string' && host.get('subdomain') || null;
403
+ const outputs = Object.keys(AI_PRIVACY_OUTPUTS).filter((key)=>written.done.some((one)=>one.output === key)).map((key, index)=>({
404
+ resource: 'privacy',
405
+ id: AI_PRIVACY_OUTPUTS[key].id,
406
+ hostId,
407
+ hostSubdomain,
408
+ label: AI_PRIVACY_OUTPUTS[key].label,
409
+ note: aiPrivacyOutputNote(written.done.filter((one)=>one.output === key).map((one)=>one.said), AI_PRIVACY_OUTPUTS[key].where, // What was not done is said once, on the first output.
410
+ index === 0 ? notDone : [], written.changedAny)
411
+ }));
412
+ if (!outputs.length) return unspent([
413
+ AI_PRIVACY_NOTHING_COPY,
414
+ ...notDone
415
+ ].join(' '));
416
+ return aiUnspentOutcome(model, {
417
+ outputs
418
+ });
419
+ };
420
+ }
421
+ export const runAiJobPrivacyStep = createAiJobPrivacyStep();
422
+ /**
423
+ * A privacy change is admitted on a site of the job's own org, for a member
424
+ * who may write it now, where the request names at least one change this can
425
+ * make. Refused in those words before anything is written.
426
+ */ export function createAiPrivacyJobAdmission(deps = {}) {
427
+ var _deps_ownerOf;
428
+ const ownerOf = (_deps_ownerOf = deps.ownerOf) != null ? _deps_ownerOf : resolveOrgIdForHost;
429
+ return async (context)=>{
430
+ var _context_uid;
431
+ if (!context.hostId) return {
432
+ status: 400,
433
+ error: AI_PRIVACY_NO_SITE_COPY
434
+ };
435
+ const owner = await ownerOf(context.hostId);
436
+ if (!owner || owner !== context.orgId) return {
437
+ status: 404,
438
+ error: 'Unknown site'
439
+ };
440
+ const read = aiPrivacyRequestOf(context.inputs);
441
+ if (aiPrivacyRequestEmpty(read.request)) return {
442
+ status: 400,
443
+ error: [
444
+ AI_PRIVACY_NOTHING_COPY,
445
+ ...read.problems
446
+ ].join(' ')
447
+ };
448
+ const host = await context.firestore.collection('hosts').doc(context.hostId).get();
449
+ const refused = await aiPrivacyAccessRefusal({
450
+ host,
451
+ uid: (_context_uid = context.uid) != null ? _context_uid : '',
452
+ org: context.org,
453
+ request: read.request
454
+ }, {
455
+ lockdown: deps.lockdown
456
+ });
457
+ return refused ? {
458
+ status: 403,
459
+ error: refused
460
+ } : null;
461
+ };
462
+ }
463
+ export const aiPrivacyJobAdmission = createAiPrivacyJobAdmission();
464
+ /** Registers the privacy step and its door (AGL-4033). */ export function registerAiPrivacyJob() {
465
+ registerAiJobStep(AI_PRIVACY_KIND, runAiJobPrivacyStep, {
466
+ minimumMs: AI_JOB_PRIVACY_STEP_MINIMUM_MS
467
+ });
468
+ registerAiJobAdmission(AI_PRIVACY_KIND, aiPrivacyJobAdmission);
469
+ }
470
+
471
+ //# sourceMappingURL=ai-job-privacy-step.js.map