@salesforce/afv-skills 1.50.0 → 1.52.0
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 +1 -1
- package/skills/experience-ui-bundle-deploy/SKILL.md +43 -2
- package/skills/experience-ui-bundle-deploy/references/config-scaffold.md +16 -3
- package/skills/experience-ui-bundle-deploy/references/logout-url.md +97 -0
- package/skills/experience-ui-bundle-deploy/scripts/set-logout-url.mjs +304 -0
- package/skills/experience-ui-bundle-localize/SKILL.md +107 -90
- package/skills/experience-ui-bundle-localize/references/angular/check-i18n-wired.sh +159 -0
- package/skills/experience-ui-bundle-localize/references/angular/i18n-setup.md +250 -0
- package/skills/experience-ui-bundle-localize/references/angular/interpolation.md +156 -0
- package/skills/experience-ui-bundle-localize/references/angular/localize.md +111 -0
- package/skills/experience-ui-bundle-localize/references/{gotchas.md → common/gotchas.md} +27 -43
- package/skills/experience-ui-bundle-localize/references/{label-xml.md → common/label-xml.md} +27 -17
- package/skills/experience-ui-bundle-localize/references/common/platform-sdk-i18n.md +169 -0
- package/skills/experience-ui-bundle-localize/references/{verifying.md → common/verifying.md} +26 -16
- package/skills/experience-ui-bundle-localize/{scripts → references/react}/check-i18n-wired.sh +8 -3
- package/skills/experience-ui-bundle-localize/references/{i18n-setup.md → react/i18n-setup.md} +48 -10
- package/skills/experience-ui-bundle-localize/references/{interpolation.md → react/interpolation.md} +4 -4
- package/skills/experience-ui-bundle-localize/references/react/localize.md +76 -0
- package/skills/experience-ui-bundle-localize/scripts/check-manifest-registered.sh +92 -24
- package/skills/experience-ui-bundle-localize/scripts/detect-framework.sh +73 -0
- package/skills/experience-ui-bundle-site-generate/SKILL.md +1 -1
- package/skills/field-service-data-capture-form-deployer-configure/SKILL.md +1 -1
- package/skills/field-service-data-capture-form-deployer-configure/references/flow-metadata-json.md +1 -1
- package/skills/field-service-data-capture-form-editor-configure/SKILL.md +1 -1
- package/skills/field-service-data-capture-reference-configure/SKILL.md +28 -15
- package/skills/field-service-foundation-setup-designer-get/SKILL.md +1 -1
- package/skills/field-service-mobile-branding-configure/SKILL.md +1 -1
- package/skills/field-service-objective-designer-configure/SKILL.md +381 -64
- package/skills/field-service-prework-brief-deployer-configure/SKILL.md +566 -3
- package/skills/field-service-scheduling-policy-designer-query/SKILL.md +1097 -57
- package/skills/field-service-sobject-create-configure/SKILL.md +222 -7
- package/skills/field-service-voice-to-form-configure/SKILL.md +11 -11
- package/skills/field-service-work-rule-designer-configure/SKILL.md +92 -107
- package/skills/service-digital-engagement-channel-configure/SKILL.md +20 -37
- package/skills/service-digital-engagement-channel-configure/assets/messaging_channel_template.xml +3 -2
- package/skills/service-digital-engagement-channel-configure/examples/asa_agent_channel.xml +4 -4
- package/skills/service-helpagent-coordinate/SKILL.md +37 -29
- package/skills/service-helpagent-coordinate/assets/help-agent-spec.md +33 -28
- package/skills/service-helpagent-coordinate/references/agent-script.md +4 -1
- package/skills/service-helpagent-coordinate/references/channel-voice.md +1 -1
- package/skills/service-helpagent-coordinate/references/channel-web-chat.md +10 -12
package/package.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: experience-ui-bundle-deploy
|
|
3
|
-
description: "MUST activate when the project has a uiBundles/*/src/ directory and the task involves deploying to an org or post-deploy org setup. Deploys a UI bundle app and runs ordered setup: org auth, build, metadata deploy, permission-set and role assignment, Experience Cloud self-registration, social login / SSO / IDP linking (Auth Providers + SAML
|
|
3
|
+
description: "MUST activate when the project has a uiBundles/*/src/ directory and the task involves deploying to an org or post-deploy org setup. Deploys a UI bundle app and runs ordered setup: org auth, build, metadata deploy, permission-set and role assignment, Experience Cloud self-registration, social login / SSO / IDP linking (Auth Providers + SAML on a React site), site logout URL, seed-data import, and GraphQL schema fetch + codegen. Trigger signals: *.network-meta.xml, org-setup.config.json (socialLogin/logoutUrl), data-plan.json, sfdx-project.json, or mentions of deploy, org setup, social login/SSO, or logout URL on an Experience site. DO NOT TRIGGER when: creating a new UI bundle project (use experience-ui-bundle-project-generate); styling pages without deploying (use experience-ui-bundle-frontend-generate); adding a feature such as auth, search, or file upload without deploying (use the matching experience-ui-bundle-*-generate skill); configuring MFA permission sets (use experience-ui-bundle-mfa-configure)."
|
|
4
4
|
metadata:
|
|
5
|
-
version: "1.
|
|
5
|
+
version: "1.3"
|
|
6
6
|
domains: ["Experience", "Developer Experience"]
|
|
7
7
|
relatedSkills:
|
|
8
8
|
- "experience-ui-bundle-frontend-generate"
|
|
@@ -112,6 +112,46 @@ of this flow.
|
|
|
112
112
|
Timeout 180s. Must complete before permission assignment and schema fetch —
|
|
113
113
|
objects, fields, and permission sets appear in the org only after deploy.
|
|
114
114
|
|
|
115
|
+
## Step 3b — Set the site logout URL (config-gated)
|
|
116
|
+
|
|
117
|
+
Run only when `org-setup.config.json` has a top-level `logoutUrl`. If absent,
|
|
118
|
+
no-op cleanly and say so. **Non-destructive and idempotent** — no ask needed.
|
|
119
|
+
|
|
120
|
+
Runs **here, after the deploy** (not folded into it) because the platform rejects
|
|
121
|
+
a relative logout URL (*"The logout page URL must be an absolute URL."*), and a
|
|
122
|
+
shipped site-relative path (e.g. `/propertyrentalapp/`) is resolved to absolute
|
|
123
|
+
against the site's Experience Cloud origin — which only exists once the site is
|
|
124
|
+
deployed.
|
|
125
|
+
|
|
126
|
+
Why it matters: a site with no `<logoutUrl>` sends a logging-out member to the
|
|
127
|
+
**org default-site login page** — in a multi-site org that's a *different*
|
|
128
|
+
community, so Sign Out lands on the wrong site's login page. Setting it steers
|
|
129
|
+
logout back to this site (reload as Guest).
|
|
130
|
+
|
|
131
|
+
Steps:
|
|
132
|
+
|
|
133
|
+
1. **Read** `logoutUrl` from config; absent → skip.
|
|
134
|
+
2. **Derive the site** — `scripts/derive-site-name.sh` (single `*.network-meta.xml`;
|
|
135
|
+
skip if zero/ambiguous).
|
|
136
|
+
3. **Resolve, set, and deploy — one command.** Invoke the helper, which resolves
|
|
137
|
+
the value to an absolute URL (site-relative → matched against the community
|
|
138
|
+
`siteUrl` **path**, never guessing), writes `<logoutUrl>` idempotently in the
|
|
139
|
+
canonical position, and deploys only that file:
|
|
140
|
+
|
|
141
|
+
```bash
|
|
142
|
+
node scripts/set-logout-url.mjs \
|
|
143
|
+
--logout-url "<logoutUrl from config>" \
|
|
144
|
+
--network-file <sourceRoot>/networks/<site>.network-meta.xml \
|
|
145
|
+
--target-org <org> --site <site> --deploy
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
**Best-effort:** the helper exits **0** on success or already-set, and **3** on a
|
|
149
|
+
recoverable skip (network file missing, origin unresolvable, XML-special char, or
|
|
150
|
+
deploy failure). Treat **exit 3 as a loud skip, not a setup failure** — continue
|
|
151
|
+
the rest of setup and tell the user to set the logout URL manually in the site's
|
|
152
|
+
Administration settings. Usage, exit-code contract, and port-provenance:
|
|
153
|
+
`references/logout-url.md`.
|
|
154
|
+
|
|
115
155
|
## Step 4 — Assign permission sets
|
|
116
156
|
|
|
117
157
|
Discover permission sets under `<packageDir>/main/default/permissionsets/`. If
|
|
@@ -305,6 +345,7 @@ developer action, not part of setup; if the user asks to preview the site, see
|
|
|
305
345
|
1. Authenticate org
|
|
306
346
|
2. Build UI bundles (pre-deploy)
|
|
307
347
|
3. Deploy metadata (deploy-license gate if self-reg configured)
|
|
348
|
+
3b. Set the site logout URL (if `logoutUrl` configured — post-deploy, idempotent, best-effort)
|
|
308
349
|
4. Assign permission sets (config-driven assignee)
|
|
309
350
|
5. Assign role (if configured)
|
|
310
351
|
6. Enable self-registration (if configured — ask first)
|
|
@@ -35,7 +35,8 @@ Every top-level key is optional; each object is strict (no extra keys).
|
|
|
35
35
|
"accountName": "<display name>" },
|
|
36
36
|
"socialLogin": { "communityMemberProfile": "<Profile name>",
|
|
37
37
|
"authProviderNames": ["<DeveloperName>", "..."],
|
|
38
|
-
"communityUserPermset": "<PermissionSet API name>" }
|
|
38
|
+
"communityUserPermset": "<PermissionSet API name>" },
|
|
39
|
+
"logoutUrl": "/<site-path>/ or https://<absolute-url>"
|
|
39
40
|
}
|
|
40
41
|
```
|
|
41
42
|
|
|
@@ -60,6 +61,14 @@ Every top-level key is optional; each object is strict (no extra keys).
|
|
|
60
61
|
`selfRegistration`, there is **no `siteName`** — the site is derived from the
|
|
61
62
|
single `networks/<siteName>.network-meta.xml`. Full step detail:
|
|
62
63
|
`references/social-login.md`.
|
|
64
|
+
- **`logoutUrl`** (optional) drives step 3b. A **site-relative path** (e.g.
|
|
65
|
+
`/propertyrentalapp/`, recommended — domain-independent) or an absolute
|
|
66
|
+
`http(s)` URL. Setup resolves a relative value to the absolute URL the platform
|
|
67
|
+
requires (against the site's Experience Cloud origin) and writes it to the
|
|
68
|
+
site's `<logoutUrl>` Network metadata after deploy, so Sign Out returns members
|
|
69
|
+
to *this* site instead of the org default site. No `siteName` — the site is
|
|
70
|
+
derived from the single `networks/<siteName>.network-meta.xml`. Full step
|
|
71
|
+
detail: `references/logout-url.md`.
|
|
63
72
|
|
|
64
73
|
## Scaffolding flow (permset folder present, config missing)
|
|
65
74
|
|
|
@@ -68,8 +77,12 @@ Every top-level key is optional; each object is strict (no extra keys).
|
|
|
68
77
|
2. **Ask the user, per permset, who to assign it to** — `currentUser`,
|
|
69
78
|
`guestUser`, or `skip`. Default a whole-set answer to `currentUser` (not
|
|
70
79
|
`skip`) since the folder exists on purpose; only use `guestUser` for permsets
|
|
71
|
-
meant for the site's guest user. Also ask whether they want a `role
|
|
72
|
-
`selfRegistration` block while you're writing the file (
|
|
80
|
+
meant for the site's guest user. Also ask whether they want a `role`,
|
|
81
|
+
`selfRegistration`, and/or `logoutUrl` block while you're writing the file (all
|
|
82
|
+
optional). For `logoutUrl`, phrase it as "point Sign Out back to this site?" —
|
|
83
|
+
if yes, default the value to the site-relative path `/<site-path>/` (step 3b
|
|
84
|
+
resolves it to absolute post-deploy). Skipping it leaves logout on the org
|
|
85
|
+
default site (see the `logoutUrl` note above).
|
|
73
86
|
3. **Offer two equivalent paths** — let the user pick:
|
|
74
87
|
- **Write the file**: copy `assets/org-setup.config.template.json` to
|
|
75
88
|
`org-setup.config.json` at the project root and fill in the answers. This is
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
# Logout URL — steer Sign Out back to this site
|
|
2
|
+
|
|
3
|
+
Detail for the **logoutUrl** step (SKILL.md step 3b). The step is executed by the
|
|
4
|
+
deterministic helper `scripts/set-logout-url.mjs` (a faithful port of the
|
|
5
|
+
reference `org-setup.mjs` logout-URL logic); this file covers **why** it exists
|
|
6
|
+
and **how** to run it, not a re-specification of the algorithm.
|
|
7
|
+
|
|
8
|
+
Source of truth for the behavior: reference `org-setup.mjs` `ensureLogoutUrl`
|
|
9
|
+
(1103-1169) and the `main()` logout-URL step (2137-2148); the pure resolution
|
|
10
|
+
helpers in `org-setup-url.mjs` (`isAbsoluteLogoutUrl`, `firstPathSegment`,
|
|
11
|
+
`pickCommunityBaseUrl`, `resolveLogoutUrl`) and the XML helpers in
|
|
12
|
+
`org-setup-xml.mjs` (`assertSafeLogoutUrl`, `setLogoutUrl`) — all reproduced in
|
|
13
|
+
`scripts/set-logout-url.mjs`. Config schema: `org-setup-config-schema.mjs`
|
|
14
|
+
(93, `logoutUrl: z.string().min(1).optional()`).
|
|
15
|
+
|
|
16
|
+
Provenance: this step ports the "logout redirects to the login page of a
|
|
17
|
+
different site" fix, added to `org-setup.mjs` but not previously reflected in this
|
|
18
|
+
skill.
|
|
19
|
+
|
|
20
|
+
Run this step **only** when a top-level `logoutUrl` is set in
|
|
21
|
+
`org-setup.config.json`. If it is absent, the step is a clean no-op — say so.
|
|
22
|
+
Unlike self-registration and data import, it is **non-destructive and
|
|
23
|
+
idempotent**, so it does **not** require asking first.
|
|
24
|
+
|
|
25
|
+
## Why it matters
|
|
26
|
+
|
|
27
|
+
An Experience Cloud site with no `<logoutUrl>` on its Network sends a logging-out
|
|
28
|
+
member to the **org default-site login page**. In an org hosting more than one
|
|
29
|
+
Experience/CLWR site, that default is a *different* community — so Sign Out drops
|
|
30
|
+
the user on the wrong site's login page. Setting `<logoutUrl>` to this site's own
|
|
31
|
+
URL steers logout back here (the user reloads as Guest on the same site).
|
|
32
|
+
|
|
33
|
+
## Why it runs AFTER deploy (step 3b, not folded into deploy)
|
|
34
|
+
|
|
35
|
+
The platform **rejects a relative logout URL** at deploy time: *"The logout page
|
|
36
|
+
URL must be an absolute URL."* But a shipped template must stay
|
|
37
|
+
domain-independent, so apps ship a **site-relative** path (e.g.
|
|
38
|
+
`"logoutUrl": "/propertyrentalapp/"`) that is valid on every org they deploy to.
|
|
39
|
+
Resolving that to the absolute URL the platform requires needs the site's
|
|
40
|
+
Experience Cloud **origin**, which is only discoverable (via the Connect
|
|
41
|
+
communities API) once the site exists — i.e. after the main deploy. An
|
|
42
|
+
already-absolute config value (`http(s)://…`) is used as-is, with no lookup.
|
|
43
|
+
|
|
44
|
+
## How to run
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
node scripts/set-logout-url.mjs \
|
|
48
|
+
--logout-url "<logoutUrl from org-setup.config.json>" \
|
|
49
|
+
--network-file <sourceRoot>/networks/<site>.network-meta.xml \
|
|
50
|
+
--target-org <org> \
|
|
51
|
+
--site <site> \
|
|
52
|
+
--deploy
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Derive `<site>` first with `scripts/derive-site-name.sh` (it prints the base name
|
|
56
|
+
of the single `*.network-meta.xml`, or exits non-zero when zero or more than one
|
|
57
|
+
exist — an ambiguous site is skipped, never guessed). Omit `--deploy` to resolve
|
|
58
|
+
and write the file **without** deploying (e.g. to inspect the change first).
|
|
59
|
+
|
|
60
|
+
### What the helper does (so you don't re-derive it)
|
|
61
|
+
|
|
62
|
+
- **Resolves** the config value to an absolute URL: already-absolute → used
|
|
63
|
+
verbatim; site-relative → its origin is matched from the org's communities
|
|
64
|
+
(`/connect/communities`, at the org's current API version), **path-first** then by site name, and it
|
|
65
|
+
refuses to fall back to an arbitrary community (resolving against the wrong
|
|
66
|
+
origin would silently produce a valid-but-wrong URL).
|
|
67
|
+
- **Writes** `<logoutUrl>` into the network metadata idempotently — replaces an
|
|
68
|
+
existing value in place, or inserts a new node in the canonical alphabetical
|
|
69
|
+
position; an already-correct value is a byte-for-byte no-op (no deploy).
|
|
70
|
+
- **Rejects** a URL containing an XML-special character (`& < > " '`) rather than
|
|
71
|
+
escaping it.
|
|
72
|
+
- **Deploys** only the one network file (with `--deploy`).
|
|
73
|
+
|
|
74
|
+
### Exit-code contract (best-effort)
|
|
75
|
+
|
|
76
|
+
| Exit | Meaning | What the step should do |
|
|
77
|
+
|------|---------|-------------------------|
|
|
78
|
+
| `0` | Applied (changed), already-correct (unchanged), or deployed | Report success; continue. |
|
|
79
|
+
| `3` | **Recoverable skip** — network file missing, origin unresolvable, XML-special char, or deploy failed | **Do not treat as a setup failure.** Continue the rest of setup; tell the user to set the logout URL manually. |
|
|
80
|
+
| `1` | Usage / argument error | Fix the invocation. |
|
|
81
|
+
|
|
82
|
+
On a skip, tell the user to **set the logout URL manually in the site's
|
|
83
|
+
Administration settings** (Setup → Digital Experiences → the site →
|
|
84
|
+
Administration → Login & Registration → Logout URL).
|
|
85
|
+
|
|
86
|
+
## Config shape
|
|
87
|
+
|
|
88
|
+
```json
|
|
89
|
+
{ "logoutUrl": "/propertyrentalapp/" }
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
- Top-level key (a sibling of `permsetAssignments` / `socialLogin`), optional.
|
|
93
|
+
- A **site-relative path** (recommended — domain-independent) or an absolute
|
|
94
|
+
`http(s)` URL. A relative value is resolved to absolute at deploy time; an
|
|
95
|
+
absolute value is used as-is.
|
|
96
|
+
- Like `selfRegistration` / `socialLogin`, there is **no `siteName`** — the site
|
|
97
|
+
is derived from the single `networks/<site>.network-meta.xml`.
|
|
@@ -0,0 +1,304 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// Resolve + set the Experience Cloud site logout URL (deploy step 3b).
|
|
3
|
+
//
|
|
4
|
+
// Deterministic helper for the "site logout URL" step: it resolves the shipped
|
|
5
|
+
// (site-relative or absolute) logoutUrl config value to the ABSOLUTE URL the
|
|
6
|
+
// platform requires, writes it idempotently into the site's network metadata,
|
|
7
|
+
// and (optionally) deploys just that file. The SKILL.md step invokes this
|
|
8
|
+
// instead of re-deriving the algorithm in prose. Faithful port of the reference
|
|
9
|
+
// org-setup.mjs helpers (org-setup-url.mjs + org-setup-xml.mjs).
|
|
10
|
+
//
|
|
11
|
+
// Why absolute + post-deploy: the platform rejects a relative logout URL at
|
|
12
|
+
// deploy time ("The logout page URL must be an absolute URL."), and a shipped
|
|
13
|
+
// site-relative path is resolved against the site's Experience Cloud origin,
|
|
14
|
+
// which only exists once the site is deployed.
|
|
15
|
+
//
|
|
16
|
+
// Usage:
|
|
17
|
+
// node scripts/set-logout-url.mjs \
|
|
18
|
+
// --logout-url "/myapp/" \
|
|
19
|
+
// --network-file <sourceRoot>/networks/<site>.network-meta.xml \
|
|
20
|
+
// --target-org <org> \
|
|
21
|
+
// [--site <siteName>] [--deploy]
|
|
22
|
+
//
|
|
23
|
+
// Exit codes:
|
|
24
|
+
// 0 applied (changed), already-correct (unchanged), or deployed
|
|
25
|
+
// 3 best-effort SKIP (file missing / origin unresolvable / XML-special char /
|
|
26
|
+
// deploy failure) — NOT a setup failure; the caller logs loudly and continues
|
|
27
|
+
// 1 usage / argument error
|
|
28
|
+
//
|
|
29
|
+
// A skip (exit 3) means: tell the user to set the logout URL manually in the
|
|
30
|
+
// site's Administration settings (Setup -> Digital Experiences -> the site ->
|
|
31
|
+
// Administration -> Login & Registration -> Logout URL).
|
|
32
|
+
|
|
33
|
+
import { readFileSync, writeFileSync, existsSync } from 'node:fs';
|
|
34
|
+
import { execFileSync } from 'node:child_process';
|
|
35
|
+
|
|
36
|
+
// Connect "communities" REST resource. Its API version is resolved from the org
|
|
37
|
+
// at runtime (see resolveApiVersion) so it never goes stale as versions
|
|
38
|
+
// increment; this pinned floor — the version used by the reference org-setup.mjs
|
|
39
|
+
// port — is the fallback used only when that lookup fails. This step reads just
|
|
40
|
+
// `siteUrl`/`name`, whose shape is stable across versions, so the exact version
|
|
41
|
+
// is not load-bearing.
|
|
42
|
+
const FALLBACK_API_VERSION = 'v62.0';
|
|
43
|
+
const XML_SPECIAL_CHARS = /[&<>"']/;
|
|
44
|
+
|
|
45
|
+
// ---- argument parsing -------------------------------------------------------
|
|
46
|
+
|
|
47
|
+
function parseArgs(argv) {
|
|
48
|
+
const args = { deploy: false };
|
|
49
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
50
|
+
const a = argv[i];
|
|
51
|
+
switch (a) {
|
|
52
|
+
case '--logout-url': args.logoutUrl = argv[++i]; break;
|
|
53
|
+
case '--network-file': args.networkFile = argv[++i]; break;
|
|
54
|
+
case '--target-org': args.targetOrg = argv[++i]; break;
|
|
55
|
+
case '--site': args.site = argv[++i]; break;
|
|
56
|
+
case '--deploy': args.deploy = true; break;
|
|
57
|
+
default:
|
|
58
|
+
fail(1, `unknown argument: ${a}`);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
if (!args.logoutUrl) fail(1, 'missing --logout-url');
|
|
62
|
+
if (!args.networkFile) fail(1, 'missing --network-file');
|
|
63
|
+
if (!args.targetOrg) fail(1, 'missing --target-org');
|
|
64
|
+
return args;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function fail(code, message) {
|
|
68
|
+
console.error(`set-logout-url: ${message}`);
|
|
69
|
+
process.exit(code);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function skip(message) {
|
|
73
|
+
console.error(
|
|
74
|
+
`set-logout-url: SKIP — ${message}. Set the logout URL manually in the site's ` +
|
|
75
|
+
`Administration settings (Login & Registration -> Logout URL).`,
|
|
76
|
+
);
|
|
77
|
+
process.exit(3);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// ---- pure URL helpers (port of org-setup-url.mjs) ---------------------------
|
|
81
|
+
|
|
82
|
+
/** True when `value` is an absolute http(s) URL — already deployable as-is. */
|
|
83
|
+
function isAbsoluteLogoutUrl(value) {
|
|
84
|
+
return /^https?:\/\//i.test(String(value).trim());
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** Leading path segment of a URL or path: "/app/" -> "app", "/" -> "". */
|
|
88
|
+
function firstPathSegment(pathOrUrl) {
|
|
89
|
+
let pathname;
|
|
90
|
+
try {
|
|
91
|
+
pathname = new URL(pathOrUrl).pathname; // absolute URL
|
|
92
|
+
} catch {
|
|
93
|
+
pathname = String(pathOrUrl).split(/[?#]/)[0]; // relative path
|
|
94
|
+
}
|
|
95
|
+
return pathname.replace(/^\/+/, '').split('/')[0];
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* Choose the community whose siteUrl anchors a relative logout path — match on
|
|
100
|
+
* the siteUrl PATH first, then on `name` == siteName. Never guesses an arbitrary
|
|
101
|
+
* community (resolving against the wrong origin yields a valid-but-wrong URL).
|
|
102
|
+
*/
|
|
103
|
+
function pickCommunityBaseUrl(communities, configLogoutUrl, siteName) {
|
|
104
|
+
const list = Array.isArray(communities) ? communities.filter((c) => c && c.siteUrl) : [];
|
|
105
|
+
const seg = firstPathSegment(configLogoutUrl);
|
|
106
|
+
if (seg) {
|
|
107
|
+
const byPath = list.find((c) => firstPathSegment(c.siteUrl) === seg);
|
|
108
|
+
if (byPath) return byPath.siteUrl;
|
|
109
|
+
}
|
|
110
|
+
if (siteName) {
|
|
111
|
+
const byName = list.find((c) => c.name === siteName);
|
|
112
|
+
if (byName) return byName.siteUrl;
|
|
113
|
+
}
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Resolve a config value to an absolute URL (relative -> against baseUrl origin). */
|
|
118
|
+
function resolveLogoutUrl(configLogoutUrl, baseUrl) {
|
|
119
|
+
const value = String(configLogoutUrl).trim();
|
|
120
|
+
if (isAbsoluteLogoutUrl(value)) return value;
|
|
121
|
+
if (!baseUrl) {
|
|
122
|
+
throw new Error(
|
|
123
|
+
`logout URL "${configLogoutUrl}" is site-relative but no Experience Cloud community ` +
|
|
124
|
+
`site URL was found to resolve it into an absolute URL`,
|
|
125
|
+
);
|
|
126
|
+
}
|
|
127
|
+
let resolved;
|
|
128
|
+
try {
|
|
129
|
+
resolved = new URL(value, baseUrl).href;
|
|
130
|
+
} catch (e) {
|
|
131
|
+
throw new Error(`could not resolve "${configLogoutUrl}" against "${baseUrl}": ${e.message}`);
|
|
132
|
+
}
|
|
133
|
+
if (!isAbsoluteLogoutUrl(resolved)) {
|
|
134
|
+
throw new Error(`resolved logout URL "${resolved}" is not an absolute http(s) URL`);
|
|
135
|
+
}
|
|
136
|
+
return resolved;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// ---- XML helpers (port of org-setup-xml.mjs setLogoutUrl) -------------------
|
|
140
|
+
|
|
141
|
+
/** Direct child element names of <Network> (depth-aware, no XML deps). */
|
|
142
|
+
function networkChildNames(xml) {
|
|
143
|
+
const open = xml.match(/<Network\b[^>]*>/);
|
|
144
|
+
const close = xml.lastIndexOf('</Network>');
|
|
145
|
+
if (!open || close === -1) return [];
|
|
146
|
+
const inner = xml.slice(open.index + open[0].length, close);
|
|
147
|
+
const tag = /<(\/?)([A-Za-z_][\w.-]*)\b[^>]*?(\/?)>/g;
|
|
148
|
+
const names = new Set();
|
|
149
|
+
let depth = 0;
|
|
150
|
+
let m;
|
|
151
|
+
while ((m = tag.exec(inner)) !== null) {
|
|
152
|
+
const [, closing, name, selfClose] = m;
|
|
153
|
+
if (closing) {
|
|
154
|
+
depth -= 1;
|
|
155
|
+
} else if (selfClose) {
|
|
156
|
+
if (depth === 0) names.add(name);
|
|
157
|
+
} else {
|
|
158
|
+
if (depth === 0) names.add(name);
|
|
159
|
+
depth += 1;
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return [...names];
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function firstChildIndent(xml) {
|
|
166
|
+
const m = xml.match(/\n([ \t]+)<[A-Za-z_]/);
|
|
167
|
+
return m ? m[1] : ' ';
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/**
|
|
171
|
+
* Returns { xml, changed }. Idempotent (unchanged when already equal); replaces
|
|
172
|
+
* an existing value in place; else inserts <logoutUrl> in the canonical
|
|
173
|
+
* alphabetical position among Network's top-level children. Throws on an
|
|
174
|
+
* XML-special char in the URL (reject, don't escape).
|
|
175
|
+
*/
|
|
176
|
+
function setLogoutUrl(xml, url) {
|
|
177
|
+
if (XML_SPECIAL_CHARS.test(String(url))) {
|
|
178
|
+
throw new Error(`logoutUrl "${url}" contains an XML-special character (& < > " ')`);
|
|
179
|
+
}
|
|
180
|
+
const node = `<logoutUrl>${url}</logoutUrl>`;
|
|
181
|
+
const existing = xml.match(/<logoutUrl>([^<]*)<\/logoutUrl>/);
|
|
182
|
+
if (existing) {
|
|
183
|
+
if (existing[1] === url) return { xml, changed: false };
|
|
184
|
+
return { xml: xml.replace(/<logoutUrl>[^<]*<\/logoutUrl>/, node), changed: true };
|
|
185
|
+
}
|
|
186
|
+
// Absent — insert before the first top-level sibling that sorts after "logoutUrl".
|
|
187
|
+
const successor = networkChildNames(xml)
|
|
188
|
+
.filter((name) => name > 'logoutUrl')
|
|
189
|
+
.sort()[0];
|
|
190
|
+
if (successor) {
|
|
191
|
+
const beforeSuccessor = new RegExp(`(\\n[ \\t]*)(<${successor}\\b)`);
|
|
192
|
+
if (beforeSuccessor.test(xml)) {
|
|
193
|
+
return { xml: xml.replace(beforeSuccessor, `$1${node}$1$2`), changed: true };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
const indent = firstChildIndent(xml);
|
|
197
|
+
const beforeClose = /(\n)([ \t]*<\/Network>)/;
|
|
198
|
+
if (beforeClose.test(xml)) {
|
|
199
|
+
return { xml: xml.replace(beforeClose, `\n${indent}${node}$1$2`), changed: true };
|
|
200
|
+
}
|
|
201
|
+
return { xml: xml.replace(/(<Network\b[^>]*>)/, `$1\n${indent}${node}`), changed: true };
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
// ---- org I/O ----------------------------------------------------------------
|
|
205
|
+
|
|
206
|
+
/**
|
|
207
|
+
* Resolve the org's highest supported REST API version (e.g. "v66.0") via the
|
|
208
|
+
* version-less `/services/data/` endpoint. Falls back to FALLBACK_API_VERSION
|
|
209
|
+
* if the lookup fails or returns nothing usable, so resolution is never worse
|
|
210
|
+
* than a fixed pin.
|
|
211
|
+
*/
|
|
212
|
+
function resolveApiVersion(targetOrg) {
|
|
213
|
+
try {
|
|
214
|
+
const out = execFileSync(
|
|
215
|
+
'sf',
|
|
216
|
+
['api', 'request', 'rest', '/services/data/', '--target-org', targetOrg],
|
|
217
|
+
{ encoding: 'utf8', timeout: 120000 },
|
|
218
|
+
);
|
|
219
|
+
const versions = JSON.parse(out);
|
|
220
|
+
const max = Array.isArray(versions)
|
|
221
|
+
? versions
|
|
222
|
+
.map((v) => parseFloat(v && v.version))
|
|
223
|
+
.filter((n) => !Number.isNaN(n))
|
|
224
|
+
.sort((a, b) => a - b)
|
|
225
|
+
.pop()
|
|
226
|
+
: undefined;
|
|
227
|
+
if (max) return `v${max.toFixed(1)}`;
|
|
228
|
+
} catch {
|
|
229
|
+
// Unreachable org, unexpected shape, etc. — fall through to the pinned floor.
|
|
230
|
+
}
|
|
231
|
+
return FALLBACK_API_VERSION;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function fetchCommunities(targetOrg) {
|
|
235
|
+
const apiVersion = resolveApiVersion(targetOrg);
|
|
236
|
+
const out = execFileSync(
|
|
237
|
+
'sf',
|
|
238
|
+
['api', 'request', 'rest', `/services/data/${apiVersion}/connect/communities`, '--target-org', targetOrg],
|
|
239
|
+
{ encoding: 'utf8', timeout: 120000 },
|
|
240
|
+
);
|
|
241
|
+
const parsed = JSON.parse(out);
|
|
242
|
+
return Array.isArray(parsed.communities) ? parsed.communities : [];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function deployNetworkFile(networkFile, targetOrg) {
|
|
246
|
+
execFileSync(
|
|
247
|
+
'sf',
|
|
248
|
+
['project', 'deploy', 'start', '--source-dir', networkFile, '--target-org', targetOrg],
|
|
249
|
+
{ stdio: 'inherit', timeout: 120000 },
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// ---- main -------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
function main() {
|
|
256
|
+
const args = parseArgs(process.argv.slice(2));
|
|
257
|
+
|
|
258
|
+
if (!existsSync(args.networkFile)) {
|
|
259
|
+
skip(`network metadata not found: ${args.networkFile}`);
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
// 1. Resolve to an absolute URL.
|
|
263
|
+
let absoluteUrl;
|
|
264
|
+
try {
|
|
265
|
+
let baseUrl = null;
|
|
266
|
+
if (!isAbsoluteLogoutUrl(args.logoutUrl)) {
|
|
267
|
+
baseUrl = pickCommunityBaseUrl(fetchCommunities(args.targetOrg), args.logoutUrl, args.site);
|
|
268
|
+
}
|
|
269
|
+
absoluteUrl = resolveLogoutUrl(args.logoutUrl, baseUrl);
|
|
270
|
+
} catch (e) {
|
|
271
|
+
skip(`cannot resolve an absolute logout URL — ${e.message}`);
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// 2. Write it into the network metadata (idempotent).
|
|
275
|
+
let result;
|
|
276
|
+
try {
|
|
277
|
+
result = setLogoutUrl(readFileSync(args.networkFile, 'utf8'), absoluteUrl);
|
|
278
|
+
} catch (e) {
|
|
279
|
+
skip(`cannot set <logoutUrl> in ${args.networkFile} — ${e.message}`);
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
if (!result.changed) {
|
|
283
|
+
console.log(`logout URL already set to "${absoluteUrl}"; no change (skipping deploy).`);
|
|
284
|
+
process.exit(0);
|
|
285
|
+
}
|
|
286
|
+
writeFileSync(args.networkFile, result.xml);
|
|
287
|
+
console.log(`set <logoutUrl>${absoluteUrl}</logoutUrl> in ${args.networkFile}`);
|
|
288
|
+
|
|
289
|
+
// 3. Deploy only that file (best-effort).
|
|
290
|
+
if (args.deploy) {
|
|
291
|
+
try {
|
|
292
|
+
deployNetworkFile(args.networkFile, args.targetOrg);
|
|
293
|
+
console.log(`deployed <logoutUrl> for "${args.site || args.networkFile}".`);
|
|
294
|
+
} catch (e) {
|
|
295
|
+
skip(
|
|
296
|
+
`failed to deploy the network file (${e.message}). If the org's Network ` +
|
|
297
|
+
`emailSenderAddress differs from the shipped value it can block this deploy`,
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
process.exit(0);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
main();
|