create-safest-tools 0.2.0 → 0.2.1

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/README.md CHANGED
@@ -21,6 +21,10 @@ npx create-safest-tools safest-resolve \
21
21
 
22
22
  The `--email-from` address must belong to a domain onboarded under Cloudflare Email Service → Email Sending. Domain onboarding permits delivery to arbitrary invited users; invitations, email verification, and forgot-password delivery use this address.
23
23
 
24
+ ### Where to find the Cloudflare Access AUD
25
+
26
+ In the Cloudflare dashboard, go to **Zero Trust → Access controls → Applications** and create a **Self-hosted and private** application named **Safest Resolve infrastructure**. Add the reports hostname with **Path** `v1/infrastructure/*` only; do not protect the entire hostname. Add or create a policy named **Safest infrastructure owners** with **Action: Allow** and an **Include → Emails** rule containing only the infrastructure-owner email supplied to `--admin-email`. Do not use **Everyone**. Create the application, select **Configure → Additional settings**, and copy the 64-character **Application Audience (AUD) Tag** into `--access-aud`. The tag stays the same unless the Access application is deleted or recreated. See Cloudflare's [application-path](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/app-paths/) and [Get your AUD tag](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/#get-your-aud-tag) instructions.
27
+
24
28
  The command creates a local project and prints a read-only infrastructure plan. It does not change Cloudflare unless `--deploy` is supplied or the generated project’s `npm run setup` command is run and explicitly confirmed.
25
29
 
26
30
  The generated installation owns:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-safest-tools",
3
- "version": "0.2.0",
3
+ "version": "0.2.1",
4
4
  "description": "Create customer-owned abuse-reporting infrastructure on Cloudflare",
5
5
  "type": "module",
6
6
  "license": "Apache-2.0",
package/src/cli.mjs CHANGED
@@ -7,6 +7,23 @@ import { scaffoldProject, validateGeneratedConfiguration } from "./scaffold.mjs"
7
7
 
8
8
  const packageRoot = fileURLToPath(new URL("..", import.meta.url));
9
9
  const defaultTemplateDirectory = resolve(packageRoot, "template");
10
+ const accessAudienceDocumentation = "https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/#get-your-aud-tag";
11
+
12
+ export function accessAudienceGuidance(publicBaseUrl = "", adminEmails = []) {
13
+ let reportsHost = "<reports-host>";
14
+ try { reportsHost = new URL(publicBaseUrl).host || reportsHost; } catch {}
15
+ const ownerEmail = adminEmails[0] || "<infrastructure-owner-email>";
16
+ return `Before entering the Cloudflare Access application AUD:
17
+ 1. In Cloudflare, go to Zero Trust > Access controls > Applications.
18
+ 2. Create a Self-hosted and private application named Safest Resolve infrastructure.
19
+ 3. Add public hostname ${reportsHost} with Path v1/infrastructure/* only.
20
+ 4. Add a policy named Safest infrastructure owners: Action Allow; Include Emails; Value ${ownerEmail}.
21
+ Do not select Everyone and do not protect the entire reporting hostname.
22
+ 5. Create the application, then select Configure > Additional settings.
23
+ 6. Copy the 64-character Application Audience (AUD) Tag.
24
+ The tag stays the same unless you delete or recreate the Access application.
25
+ Cloudflare guide: ${accessAudienceDocumentation}`;
26
+ }
10
27
 
11
28
  function usage() {
12
29
  return `Create customer-owned abuse-reporting infrastructure on Cloudflare
@@ -32,6 +49,8 @@ Options:
32
49
  --help Show this help
33
50
  --version Show the package version
34
51
 
52
+ ${accessAudienceGuidance()}
53
+
35
54
  This product does not proxy or inspect application requests. Applications submit
36
55
  reports explicitly through the widget, public form, or server API.
37
56
  `;
@@ -88,12 +107,15 @@ async function ask(input, prompt, fallback = "") {
88
107
  return answer || fallback;
89
108
  }
90
109
 
91
- async function completeInteractive(options, input) {
110
+ async function completeInteractive(options, input, output) {
92
111
  options.installationName ||= await ask(input, "Installation name", "safest-resolve");
93
112
  options.publicBaseUrl ||= await ask(input, "Public reports origin", "https://reports.example.com");
94
113
  if (!options.allowedOrigins.length) options.allowedOrigins.push(await ask(input, "Application origin allowed to embed the report form", "https://app.example.com"));
95
114
  if (!options.adminEmails.length) options.adminEmails.push(await ask(input, "Infrastructure owner email"));
96
- options.accessAudience ||= await ask(input, "Cloudflare Access application AUD (leave placeholder if it is not created yet)", "replace-with-the-cloudflare-access-application-aud");
115
+ if (!options.accessAudience) {
116
+ output.log(`\n${accessAudienceGuidance(options.publicBaseUrl, options.adminEmails)}\n`);
117
+ options.accessAudience = await ask(input, "Cloudflare Access application AUD (press Enter to use a deployment-blocking placeholder)", "replace-with-the-cloudflare-access-application-aud");
118
+ }
97
119
  options.emailFromAddress ||= await ask(input, "Sender address on a Cloudflare Email Sending domain");
98
120
  return options;
99
121
  }
@@ -106,7 +128,7 @@ export async function runCli(argv, dependencies = {}) {
106
128
  const interactive = dependencies.interactive ?? (process.stdin.isTTY && !options.yes);
107
129
  if (interactive) {
108
130
  const input = createInterface({ input: process.stdin, output: process.stdout });
109
- try { await completeInteractive(options, input); } finally { input.close(); }
131
+ try { await completeInteractive(options, input, output); } finally { input.close(); }
110
132
  }
111
133
  const configuration = buildConfiguration(options);
112
134
  const templateDirectory = dependencies.templateDirectory ?? defaultTemplateDirectory;
@@ -9,7 +9,7 @@ This project deploys one Worker, one D1 database, four private R2 buckets, a Dyn
9
9
  Before deployment:
10
10
 
11
11
  1. Review `reports.config.json` and `npm run setup:plan`.
12
- 2. Create a Cloudflare Access self-hosted application for `<reports-host>/v1/infrastructure/*`; put its 64-character AUD in `access.audience` and keep `access.ownerEmails` restricted to infrastructure owners.
12
+ 2. In Cloudflare, go to **Zero Trust → Access controls → Applications** and create a **Self-hosted and private** application named **Safest Resolve infrastructure**. Add the reports hostname with **Path** `v1/infrastructure/*` only; do not protect the entire hostname. Add a reusable policy named **Safest infrastructure owners** with **Action: Allow** and an **Include → Emails** rule containing only the email addresses in `access.ownerEmails`; do not use **Everyone**. Create the application, select **Configure → Additional settings**, and copy the 64-character **Application Audience (AUD) Tag** into `access.audience`. The tag stays stable unless the Access application is deleted or recreated. See Cloudflare's [application-path](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/app-paths/), [policy](https://developers.cloudflare.com/cloudflare-one/access-controls/policies/policy-management/), and [Get your AUD tag](https://developers.cloudflare.com/cloudflare-one/access-controls/applications/http-apps/authorization-cookie/validating-json/#get-your-aud-tag) instructions.
13
13
  3. Under Cloudflare Email Service → Email Sending, onboard the configured sender domain. Password verification, invitations, and password recovery to arbitrary recipients depend on domain onboarding.
14
14
  4. Run `npm run setup`. The guided setup signs in through Wrangler, verifies Workers Paid before provisioning, creates owner-only local secrets, and walks through optional Google, GitHub, and Cloudflare OAuth credentials with exact callback URLs.
15
15
  5. Bootstrap the infrastructure owner, then invite administrators and analysts from People. Invited users do not need Cloudflare accounts and can upload a JPEG, PNG, or WebP profile picture up to 2 MB. Names and workspace roles are always displayed separately.
@@ -3,11 +3,15 @@ import { spawn } from "node:child_process";
3
3
  import { readFile, writeFile } from "node:fs/promises";
4
4
  import { resolve } from "node:path";
5
5
 
6
- function runProcess(program, args, { cwd, inherit = false } = {}) {
6
+ export function wranglerEnvironment(logLevel = "none", env = process.env) {
7
+ return { ...env, WRANGLER_LOG: logLevel, WRANGLER_WRITE_LOGS: "false" };
8
+ }
9
+
10
+ function runProcess(program, args, { cwd, inherit = false, wranglerLog = "none" } = {}) {
7
11
  return new Promise((resolvePromise, reject) => {
8
12
  const child = spawn(program, args, {
9
13
  cwd,
10
- env: { ...process.env, WRANGLER_LOG: "none" },
14
+ env: wranglerEnvironment(wranglerLog),
11
15
  stdio: inherit ? "inherit" : ["ignore", "pipe", "pipe"],
12
16
  });
13
17
  let stdout = "";
@@ -113,7 +117,10 @@ export async function ensureWranglerAuthentication({ wrangler, projectRoot, inte
113
117
  if (identity.loggedIn !== true || !Array.isArray(identity.accounts) || !identity.accounts.length) {
114
118
  throw new Error("Wrangler is authenticated but no accessible Cloudflare account was found.");
115
119
  }
116
- const tokenResult = await runner(process.execPath, [wrangler, "auth", "token", "--json"], { cwd: projectRoot });
120
+ // Wrangler emits this JSON through its logger. `WRANGLER_LOG=none` suppresses
121
+ // the response in Wrangler 4.127+, so allow log output only for this captured
122
+ // command. Disable Wrangler's disk logs so the OAuth token stays in memory.
123
+ const tokenResult = await runner(process.execPath, [wrangler, "auth", "token", "--json"], { cwd: projectRoot, wranglerLog: "log" });
117
124
  const token = jsonOutput(tokenResult, "Wrangler token lookup");
118
125
  if (typeof token.token !== "string" || !token.token) throw new Error("Wrangler did not provide an API token for preflight checks.");
119
126
  return { identity, token: token.token };