@impeccable/reviews 0.1.32 → 0.1.34
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/dist/host-questions.js +9 -3
- package/dist/playbook.js +1 -1
- package/dist/tools.js +31 -29
- package/package.json +1 -1
package/dist/host-questions.js
CHANGED
|
@@ -292,9 +292,15 @@ export function vercelBypassConsent(project, app) {
|
|
|
292
292
|
return screen;
|
|
293
293
|
return { ...screen, hint: bypassHint(app ? `${app}'s previews, built by the Vercel project ${project},` : `the previews of the Vercel project ${project}`, `${project}'s`) };
|
|
294
294
|
}
|
|
295
|
-
/**
|
|
296
|
-
|
|
297
|
-
|
|
295
|
+
/**
|
|
296
|
+
* The consent to a service token, naming, once it was found (#357), the Access application in front of the preview, its
|
|
297
|
+
* Cloudflare account, and whose previews at which host it keeps; `why` first, where it replaces a token saved for those
|
|
298
|
+
* previews that stopped opening them (REN-1194).
|
|
299
|
+
*/
|
|
300
|
+
export function cloudflareAccessConsent(found, host, app, why) {
|
|
301
|
+
const screen = HOST_SCREENS['cloudflare-access'];
|
|
302
|
+
const hint = found ? accessHint(`the Access application "${found.application}", in the Cloudflare account ${found.account}, keeps ${app ? `${app}'s previews` : 'yours'} (${host})`, `"${found.application}"'s`) : screen.hint;
|
|
303
|
+
return { ...screen, hint: why ? `${why}\n\n${hint}` : hint };
|
|
298
304
|
}
|
|
299
305
|
/** The one consent to making the Access application's own policies reusable, which the run's way through cf needs to add the token's policy (#327): the application named. */
|
|
300
306
|
export function cloudflareAccessReusable(application) {
|
package/dist/playbook.js
CHANGED
|
@@ -34,7 +34,7 @@ Set the repository up with whatever the host's CLI can do (Netlify's, Vercel's,
|
|
|
34
34
|
1. **Read the state.** The setup API's instructions and state came with the first message, trimmed to this setup; impeccable_state reads the state again. Done when you know the preview item's status, \`file\`, \`workflowStanding\` and \`hosting\` (each app's host, live address and the previews GitHub recorded, as Impeccable already holds them: a start, which the host tools take and the proofs still prove; absent from an older setup API), whether there is a \`signin\` item, whether there is an \`apps\` item (the monorepo case comes first then), and the titles in \`notPartOfThisSetup\`.
|
|
35
35
|
2. **Read the repository**: manifests, the lockfile, scripts, framework config, CI workflows, env examples, the routes, the auth code and the seed. Done when you can name the package manager, the install, build and start commands, whether the pages need a server, how people sign in, and which pages a visitor opens without signing in.
|
|
36
36
|
3. **Set up the preview**, starting with the host \`hosting\` names for the app (its case and its tool first; GitHub's record decides, so a named host whose tool lists nothing sends you on to the next): as the vercel-previews case says when impeccable_hosted_previews lists Vercel's previews of the pull requests, as the netlify-previews case says when impeccable_netlify_previews lists Netlify's, or a site of this repository with Deploy Previews on (and as preview-protection says when the host's login or password stands in front of either), as the cloudflare-previews case says when impeccable_cloudflare_previews lists Cloudflare's (a Pages project's, or a Worker's), as the render-previews case says when impeccable_render_previews answers a Render site, as the vercel-setup case says when the checkout is set up for Vercel and Vercel has never deployed it, as the netlify-setup case says when the checkout is set up for Netlify and it lists none, else as the server-previews case says; then the live app's address as the live-app-address case says, in the method's write where it can be. Done when those cases' steps are done.
|
|
37
|
-
4. **Set up the sign-in** as the sign-in case says, when the app has its own. Done when that case's steps are done.
|
|
37
|
+
4. **Set up the sign-in** as the sign-in case says, when the app has its own. When step 2 found no sign-in of any kind in the app (no form, OAuth, magic link or single sign-on) and the state's \`signin\` item, or its \`perApp\` entry for the app, reads \`sign-in-unknown\`, choose No sign-in yourself: impeccable_write to \`preview\` with \`{"auth": {"method": "none"}}\`, and the app's \`appKey\` in a monorepo. It never goes in \`left\`. Done when that case's steps are done, or No sign-in is saved.
|
|
38
38
|
5. **Run impeccable_checkup**, do what \`next\` says, and run it again. Done when \`status\` is complete, or all that is left is in \`forPerson\` or \`waitingFor\`.
|
|
39
39
|
6. **Open the pull requests** as the pull-requests case says. What waits on the person holds neither back. Done when that case's steps are done.
|
|
40
40
|
7. **Report**: impeccable_progress with every task's final status, then impeccable_report as the report section says. Done when impeccable_report answered. The web page follows Impeccable's review, so never wait for it.
|
package/dist/tools.js
CHANGED
|
@@ -16,7 +16,7 @@ import { checkoutNames, liveShaped, netlifySiteOf, previewHostName, provePreview
|
|
|
16
16
|
import { closedPulls, deploymentsOf, hostingApp, hostingRead, liveHostOf, renderSitesOf } from './hosting.js';
|
|
17
17
|
import { enableRenderPreviews, parseRenderDeployment, readRenderService, RENDER_DASHBOARD, renderSitesNamed } from './render.js';
|
|
18
18
|
import { cloudflareFromHosting, cfRunner, cfStatus, CLOUDFLARE_APP_PAGE, connectCloudflareProject, enableCloudflarePreviews, pagesProject, pagesProjectName, readCloudflarePreviews } from './cloudflare.js';
|
|
19
|
-
import { passCloudflareAccess } from './cloudflare-access.js';
|
|
19
|
+
import { passCloudflareAccess, TOKEN_NAME } from './cloudflare-access.js';
|
|
20
20
|
import { netlifyFromHosting, commandRunner, connectNetlifySite, liveSite, netlifyStatus, netlifyTeams, readNetlifyPreviews, waitForNetlifyAccess } from './netlify.js';
|
|
21
21
|
import { netlifyToken, passNetlifyLock, setSitePassword } from './netlify-password.js';
|
|
22
22
|
import { connectVercelProject, lockedDeploymentHosts, prepareFirstDeployment, projectMadeDeployment, readLinkedProject, readOwnProject, vercelDashboardScope, vercelPreviewsFact, vercelProjectFor, vercelProjectPage, vercelRepoProject, vercelStatus, vercelTeams, waitForVercelAccess, waitForVercelLock } from './vercel.js';
|
|
@@ -213,6 +213,8 @@ function plainReason(text) {
|
|
|
213
213
|
}
|
|
214
214
|
/** Names as a reader lists them: "a", "a and b", "a, b and c". */
|
|
215
215
|
const andList = (names) => (names.length > 1 ? `${names.slice(0, -1).join(', ')} and ${names.at(-1)}` : names.join(''));
|
|
216
|
+
/** The end screen's line for a Cloudflare service token the run created: what it made, on which Access application, and where it is revoked (REN-1187). */
|
|
217
|
+
const accessNote = (application) => `Impeccable created the Cloudflare service token "${TOKEN_NAME}" and its policy on the Access application "${application}". Revoke it in Zero Trust → Access controls → Service credentials; reviews stop until a new one is saved.`;
|
|
216
218
|
/** The end screen's one line for the Netlify sites whose password the run set and saved, set but did not save, or saved as it was: every site in one line (#419). */
|
|
217
219
|
const PASSWORD_NOTES = {
|
|
218
220
|
set: (sites) => sites.length === 1
|
|
@@ -1831,15 +1833,15 @@ export function createTools(ctx) {
|
|
|
1831
1833
|
const passwordReplaceConsented = new Set();
|
|
1832
1834
|
/** The address impeccable_prove_preview met Cloudflare Access's login at, of a Pages preview impeccable_cloudflare_previews listed, and the words it looked for there. */
|
|
1833
1835
|
let accessGated;
|
|
1834
|
-
/**
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
|
|
1838
|
-
|
|
1836
|
+
/**
|
|
1837
|
+
* The service token that opened each Pages project's (or Worker's) previews in this run, by project: the token, what the
|
|
1838
|
+
* tool said of it and whether the store holds it. Once it does, the proofs of that project's previews send it; never the
|
|
1839
|
+
* model. A monorepo's apps, each a project behind an Access application of its own, keep theirs side by side, so one
|
|
1840
|
+
* app's token never takes the place of another's (REN-1194).
|
|
1841
|
+
*/
|
|
1842
|
+
const accessTokens = new Map();
|
|
1839
1843
|
/** What impeccable_cloudflare_access answered, less the token: a skip is asked once, so that answer stands. */
|
|
1840
1844
|
let accessOutcome;
|
|
1841
|
-
/** The Access applications, by id, the person said yes to a token for. */
|
|
1842
|
-
const accessConsented = new Set();
|
|
1843
1845
|
/** What each host's settings last said of its pull-request previews, as the evidence tools read them: the one fact impeccable_enable_previews acts on. */
|
|
1844
1846
|
const previewFacts = new Map();
|
|
1845
1847
|
/** On a repository with several apps, the project or site each app's own read named (`<host>/<appKey>`): a no on that project ends the setup of the apps on it alone (#413). */
|
|
@@ -2840,10 +2842,12 @@ export function createTools(ctx) {
|
|
|
2840
2842
|
}
|
|
2841
2843
|
if (testAccounts && response.ok)
|
|
2842
2844
|
retry.signedIn?.saved.add(appsRepo ? String(object(p.body)?.appKey) : '');
|
|
2843
|
-
// The accounts the save signed in with, said on screen: the tasks show no status line (#417).
|
|
2845
|
+
// The accounts the save signed in with, said on screen: the tasks show no status line (#417). The web page moves on with it, not at the model's next status (REN-1183).
|
|
2844
2846
|
const signedIn = testAccounts && response.ok ? signedInLine(response.data, appsRepo ? appName(String(object(p.body)?.appKey)) : undefined) : undefined;
|
|
2845
|
-
if (signedIn)
|
|
2847
|
+
if (signedIn) {
|
|
2846
2848
|
reportDone(signedIn);
|
|
2849
|
+
ctx.sync.update({ status: signedIn });
|
|
2850
|
+
}
|
|
2847
2851
|
// A check that answered, a preview not running yet included, ran for its app; one still turned away did not.
|
|
2848
2852
|
if (check && (response.ok || response.status === 409))
|
|
2849
2853
|
checkedApps.add(typeof object(p.body)?.appKey === 'string' ? String(object(p.body)?.appKey) : '');
|
|
@@ -3441,7 +3445,8 @@ export function createTools(ctx) {
|
|
|
3441
3445
|
const site = netlifySiteOf(url) ?? known?.site;
|
|
3442
3446
|
// A saved service token opens its Pages project's previews, or its Worker's: Access admits it wherever the application's policy covers.
|
|
3443
3447
|
const project = pagesProject(origin) ?? workerPatterns.get(origin);
|
|
3444
|
-
const
|
|
3448
|
+
const accessHeld = project ? accessTokens.get(project) : undefined;
|
|
3449
|
+
const token = accessHeld?.stored ? accessHeld.value : undefined;
|
|
3445
3450
|
const prove = () => provePreview(url, p.expect, pi.cwd, {
|
|
3446
3451
|
fetch: ctx.previewFetch, signal, live: live ? (known?.host ?? true) : false,
|
|
3447
3452
|
// The project's bypass secret opens the project's own addresses: the previews it proved, and the production address read from the same project.
|
|
@@ -4160,25 +4165,23 @@ export function createTools(ctx) {
|
|
|
4160
4165
|
};
|
|
4161
4166
|
let answer;
|
|
4162
4167
|
const project = pagesProject(new URL(at.url).origin) ?? workerPatterns.get(new URL(at.url).origin);
|
|
4163
|
-
// The token in hand goes only to
|
|
4164
|
-
|
|
4168
|
+
// The project's own token in hand goes only to that project, as the proofs send it: any other address is read for an application of the person's first.
|
|
4169
|
+
const held = project ? accessTokens.get(project) : undefined;
|
|
4170
|
+
if (held && (await prove(held.value)).proven) {
|
|
4165
4171
|
// The token in hand opens this address too: stored already, or stored now.
|
|
4166
|
-
const failed =
|
|
4167
|
-
answer = { status: failed ? 'save-failed' : 'saved', ...
|
|
4172
|
+
const failed = held.stored ? null : await save(held.value);
|
|
4173
|
+
answer = { status: failed ? 'save-failed' : 'saved', ...held.made, ...(failed ? { reason: failed } : {}), value: held.value };
|
|
4168
4174
|
}
|
|
4169
4175
|
else {
|
|
4176
|
+
// A token saved for this project that no longer opens its preview is replaced only on a new yes, which the screen asks saying why (REN-1194).
|
|
4177
|
+
const why = held && `The service token saved for ${at.app ? appName(at.app.appKey) : new URL(at.url).host} no longer opens ${new URL(at.url).host}: ${proof?.reason ?? 'Cloudflare Access did not let it in.'} A new one takes its place.`;
|
|
4170
4178
|
answer = await passCloudflareAccess({
|
|
4171
4179
|
url: at.url,
|
|
4172
|
-
// Named once the Access application was found (#357)
|
|
4180
|
+
// Named once the Access application was found (#357). Every token made is asked for: a yes makes one token, for this app's previews.
|
|
4173
4181
|
consent: async (found) => {
|
|
4174
|
-
if (found && accessConsented.has(found.id))
|
|
4175
|
-
return true;
|
|
4176
4182
|
// The account by the name the last screen gives it, where a read found it; else its id (#390).
|
|
4177
4183
|
const named = found && { ...found, account: accountOf.get(`cloudflare account ${found.account}`) ?? found.account };
|
|
4178
|
-
|
|
4179
|
-
if (yes && found)
|
|
4180
|
-
accessConsented.add(found.id);
|
|
4181
|
-
return yes;
|
|
4184
|
+
return !(await ask(sentTo(cloudflareAccessConsent(named, new URL(at.url).host, at.app && appName(at.app.appKey), why), at.url))).skipped;
|
|
4182
4185
|
},
|
|
4183
4186
|
cf: cfRunner({ checkout: pi.cwd }, hostRunner),
|
|
4184
4187
|
login: cfRunner({ browser: true, timeoutMs: 300_000, checkout: pi.cwd }, hostRunner),
|
|
@@ -4202,15 +4205,14 @@ export function createTools(ctx) {
|
|
|
4202
4205
|
});
|
|
4203
4206
|
}
|
|
4204
4207
|
const { value, ...told } = answer;
|
|
4208
|
+
// A token the run made stays in the person's Cloudflare account, saved or not: the end screen says so, and where it is revoked (REN-1187).
|
|
4209
|
+
if (value && told.token === 'created' && told.application)
|
|
4210
|
+
ctx.onKeyCreated?.(accessNote(told.application), `cloudflare-access:${told.application}`);
|
|
4205
4211
|
if (value) {
|
|
4206
|
-
// What is in hand follows the store, which holds
|
|
4207
|
-
|
|
4208
|
-
accessProjects.clear();
|
|
4209
|
-
accessToken = value;
|
|
4210
|
-
accessStored = told.status === 'saved';
|
|
4211
|
-
accessMade = { ...(told.token ? { token: told.token } : {}), ...(told.application ? { application: told.application } : {}), ...(told.expires ? { expires: told.expires } : {}), ...(told.apiToken ? { apiToken: told.apiToken } : {}) };
|
|
4212
|
+
// What is in hand follows the store, which holds each app's own: the token saved now is this project's, beside every other project's.
|
|
4213
|
+
const made = { ...(told.token ? { token: told.token } : {}), ...(told.application ? { application: told.application } : {}), ...(told.expires ? { expires: told.expires } : {}), ...(told.apiToken ? { apiToken: told.apiToken } : {}) };
|
|
4212
4214
|
if (project)
|
|
4213
|
-
|
|
4215
|
+
accessTokens.set(project, { value, made, stored: told.status === 'saved' });
|
|
4214
4216
|
}
|
|
4215
4217
|
const pattern = told.status === 'saved' ? (proof?.pattern ?? workerPatterns.get(new URL(at.url).origin)) : undefined;
|
|
4216
4218
|
accessOutcome = { ...told, ...(told.status === 'saved' ? { proven: true, ...(pattern ? { pattern } : {}) } : {}) };
|