@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.
@@ -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
- /** The consent to a service token, asked once the Access application in front of the preview was found (#357): that application, its Cloudflare account, and whose previews at which host it keeps. */
296
- export function cloudflareAccessConsent(found, host, app) {
297
- return { ...HOST_SCREENS['cloudflare-access'], hint: accessHint(`the Access application "${found.application}", in the Cloudflare account ${found.account}, keeps ${app ? `${app}'s previews` : 'yours'} (${host})`, `"${found.application}"'s`) };
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
- /** The service token that opened a preview in this run, what the tool said of it, the Pages projects it opened and whether the store holds it: once it does, the proofs of those projects' previews send it; never the model. */
1835
- let accessToken;
1836
- let accessMade = {};
1837
- const accessProjects = new Set();
1838
- let accessStored = false;
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 token = accessToken && accessStored && project && accessProjects.has(project) ? accessToken : undefined;
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 a project it opened already, as the proofs send it: any other address is read for an application of the person's first.
4164
- if (accessToken && project && accessProjects.has(project) && (await prove(accessToken)).proven) {
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 = accessStored ? null : await save(accessToken);
4167
- answer = { status: failed ? 'save-failed' : 'saved', ...accessMade, ...(failed ? { reason: failed } : {}), value: accessToken };
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), and asked once per application: a yes stands for another app's previews it covers.
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
- const yes = !(await ask(sentTo(named ? cloudflareAccessConsent(named, new URL(at.url).host, at.app && appName(at.app.appKey)) : HOST_SCREENS['cloudflare-access'], at.url))).skipped;
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 one token: a new one is the one saved now, and the projects the last one opened no longer stand.
4207
- if (value !== accessToken)
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
- accessProjects.add(project);
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 } : {}) } : {}) };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@impeccable/reviews",
3
- "version": "0.1.32",
3
+ "version": "0.1.34",
4
4
  "description": "Set up a repository for Impeccable for PRs from your terminal.",
5
5
  "license": "SEE LICENSE IN LICENSE",
6
6
  "private": false,