@aglyn/plugins-ai 1.0.0-beta.277 → 1.0.0-beta.282
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +17 -17
- package/src/lib/activity/ai-activity-actions.d.ts +2 -26
- package/src/lib/activity/ai-activity-actions.js +5 -42
- package/src/lib/activity/ai-activity-actions.js.map +1 -1
- package/src/lib/activity/ai-activity-words.d.ts +42 -0
- package/src/lib/activity/ai-activity-words.js +63 -0
- package/src/lib/activity/ai-activity-words.js.map +1 -0
- package/src/lib/components/ai-insight-card.component.d.ts +2 -6
- package/src/lib/components/ai-insight-card.component.js +2 -6
- package/src/lib/components/ai-insight-card.component.js.map +1 -1
- package/src/lib/components/ai-insight-dialog.component.js +22 -32
- package/src/lib/components/ai-insight-dialog.component.js.map +1 -1
- package/src/lib/components/ai-job-links.js +15 -0
- package/src/lib/components/ai-job-links.js.map +1 -1
- package/src/lib/components/assist-panel.component.js +3 -1
- package/src/lib/components/assist-panel.component.js.map +1 -1
- package/src/lib/jobs/ai-build-capabilities.js +23 -0
- package/src/lib/jobs/ai-build-capabilities.js.map +1 -1
- package/src/lib/jobs/ai-job-build-step.js +2 -1
- package/src/lib/jobs/ai-job-build-step.js.map +1 -1
- package/src/lib/jobs/ai-job-import-step.d.ts +1 -0
- package/src/lib/jobs/ai-job-import-step.js +17 -11
- package/src/lib/jobs/ai-job-import-step.js.map +1 -1
- package/src/lib/jobs/ai-job-plan-step.js +3 -2
- package/src/lib/jobs/ai-job-plan-step.js.map +1 -1
- package/src/lib/jobs/ai-job-privacy-step.d.ts +184 -0
- package/src/lib/jobs/ai-job-privacy-step.js +471 -0
- package/src/lib/jobs/ai-job-privacy-step.js.map +1 -0
- package/src/lib/jobs/ai-site-inspiration.d.ts +10 -0
- package/src/lib/jobs/ai-site-inspiration.js +15 -0
- package/src/lib/jobs/ai-site-inspiration.js.map +1 -1
- package/src/lib/model/ai-insight-ask.d.ts +57 -0
- package/src/lib/model/ai-insight-ask.js +64 -0
- package/src/lib/model/ai-insight-ask.js.map +1 -0
- package/src/lib/model/ai-insight.d.ts +1 -6
- package/src/lib/model/ai-insight.js +2 -8
- package/src/lib/model/ai-insight.js.map +1 -1
- package/src/lib/model/ai-job-activity.js +1 -0
- package/src/lib/model/ai-job-activity.js.map +1 -1
- package/src/lib/model/ai-jobs.types.d.ts +5 -1
- package/src/lib/model/ai-jobs.types.js +1 -0
- package/src/lib/model/ai-jobs.types.js.map +1 -1
- package/src/lib/model/ai-permissions.js +6 -2
- package/src/lib/model/ai-permissions.js.map +1 -1
- package/src/lib/model/ai-site-import.d.ts +2 -0
- package/src/lib/model/ai-site-import.js +2 -0
- package/src/lib/model/ai-site-import.js.map +1 -1
- package/src/lib/model/ai-site-kinds.d.ts +10 -28
- package/src/lib/model/ai-site-kinds.js +12 -496
- package/src/lib/model/ai-site-kinds.js.map +1 -1
- package/src/lib/providers/anthropic.js +23 -14
- package/src/lib/providers/anthropic.js.map +1 -1
- package/src/lib/providers/contract.d.ts +39 -4
- package/src/lib/providers/contract.js +28 -0
- package/src/lib/providers/contract.js.map +1 -1
- package/src/lib/providers/routing.d.ts +3 -0
- package/src/lib/providers/routing.js +4 -3
- package/src/lib/providers/routing.js.map +1 -1
- package/src/lib/runtime/ai-dev-env.d.ts +3 -2
- package/src/lib/runtime/ai-dev-env.js +3 -2
- package/src/lib/runtime/ai-dev-env.js.map +1 -1
- package/src/lib/runtime/ai-page-links.d.ts +5 -2
- package/src/lib/runtime/ai-page-links.js +11 -3
- package/src/lib/runtime/ai-page-links.js.map +1 -1
- package/src/lib/runtime/ai-runtime.js +5 -2
- package/src/lib/runtime/ai-runtime.js.map +1 -1
- package/src/lib/server/assist-chat.js +58 -30
- package/src/lib/server/assist-chat.js.map +1 -1
- package/src/lib/server.js +2 -0
- package/src/lib/server.js.map +1 -1
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.d.ts +3 -1
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.js +19 -1
- package/src/lib/site-import/fixtures/site-import-crawl-recordings.js.map +1 -1
- package/src/lib/site-import/site-import-page.d.ts +2 -2
- package/src/lib/site-import/site-import-page.js +11 -1
- 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
|