@davidvornholt/standards 0.26.1 → 0.26.3

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
@@ -97,7 +97,7 @@ bun standards structure --profile source
97
97
 
98
98
  `doctor` checks project-owned integration points such as `biome.jsonc`, `AGENTS.local.md`, package scripts, sync policy, settings overlays, and Dependabot inputs.
99
99
 
100
- `structure` checks the exact Bun declaration, workspace scripts, internal dependency versions, package exports, TypeScript inheritance, accessibility wiring, workspace READMEs, and CI secret structure. The `source` profile records the few intentional differences in this repository itself.
100
+ `structure` checks the exact Bun declaration, workspace scripts, internal dependency versions, package exports, TypeScript inheritance, accessibility wiring, and CI secret structure. The `source` profile records the few intentional differences in this repository itself.
101
101
 
102
102
  ## dependabot
103
103
 
@@ -263,3 +263,5 @@ The worker accepts only exact issue or draft-PR revisions approved by a user wit
263
263
  | `GH_TOKEN` or `GITHUB_TOKEN` | GitHub API authentication where required. |
264
264
 
265
265
  The [root README](../../README.md) gives the short overview. The [adoption guide](../../docs/adoption.md) and [sync guide](../../docs/sync-and-ownership.md) explain repository ownership and first use.
266
+
267
+ The poller timers use monotonic intervals: after boot, acknowledgement starts after one minute and work starts after two minutes. Missed downtime intervals are not replayed; each tick reads the current queue. The service budget reserves thirty minutes for scanning and setup in addition to its configured agent budget, with at most twelve watched repositories per poller. Split larger lists across pollers with independent API budgets; unusually slow provider or clone operations can exhaust that overhead and require investigating the service timeout before retrying.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@davidvornholt/standards",
3
- "version": "0.26.1",
3
+ "version": "0.26.3",
4
4
  "description": "Bootstrap, synchronize, and validate davidvornholt/standards consumers",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -52,7 +52,6 @@
52
52
  "src/creds-plan-report.ts",
53
53
  "src/creds-plan-run.ts",
54
54
  "src/creds-plan-revoke.ts",
55
- "src/creds-plan-types.ts",
56
55
  "src/creds-plan-unmanaged.ts",
57
56
  "src/creds-prompt.ts",
58
57
  "src/creds-r2.ts",
@@ -166,7 +165,6 @@
166
165
  "src/structure-check.ts",
167
166
  "src/structure-bun-version.ts",
168
167
  "src/structure-profile.ts",
169
- "src/structure-readme.ts",
170
168
  "src/structure-secret-document.ts",
171
169
  "src/structure-script.ts",
172
170
  "src/structure-secrets.ts",
package/src/cli.ts CHANGED
@@ -1189,7 +1189,7 @@ const parseArgs = (argv: ReadonlyArray<string>): CliOptions => {
1189
1189
 
1190
1190
  // Canonical monorepo structure gate: workspace and root script shapes,
1191
1191
  // internal versioning, `exports`, tsconfig inheritance, a11y wiring,
1192
- // repo-owned workspace READMEs, and the undecrypted CI secrets contract
1192
+ // and the undecrypted CI secrets contract
1193
1193
  // (SOPS shape mirroring plus the keys the synced workflows require).
1194
1194
  // The `source` profile instead pins the standards template repository's own
1195
1195
  // deliberate exceptions to the consumer contract.
@@ -24,7 +24,7 @@ export const findManagedDestinationCollision = async (
24
24
  return `account ${failed.accountId}: ${failed.listed.problem}; cannot prove the destination is unambiguous`;
25
25
  }
26
26
  const wanted = destinationFootprint(format, context.dest.key);
27
- const collisions = listings.flatMap(({ accountId, listed }) =>
27
+ const collisions = listings.flatMap(({ listed }) =>
28
28
  listed.ok
29
29
  ? listed.value.flatMap((token) => {
30
30
  const ref = parseTokenName(token.name, context.repo);
@@ -35,7 +35,7 @@ export const findManagedDestinationCollision = async (
35
35
  wanted,
36
36
  inferredDestinationFootprint(keys, ref.key),
37
37
  ))
38
- ? [`${accountId}/${token.id} (${ref.key})`]
38
+ ? [`${ref.target}:${ref.key}`]
39
39
  : [];
40
40
  })
41
41
  : [],
@@ -31,8 +31,8 @@ export const runCredsAddGithub = async (
31
31
  appIdPath,
32
32
  privateKeyPath,
33
33
  ]);
34
- if (blocked !== null) {
35
- console.error(`standards creds: ${blocked}`);
34
+ if (blocked.problem !== null) {
35
+ console.error(`standards creds: ${blocked.problem}`);
36
36
  return false;
37
37
  }
38
38
  const storePath = resolveBrokerPath();
@@ -16,12 +16,21 @@ export const inspectDestinations = async (
16
16
  consumer: string,
17
17
  rel: string,
18
18
  paths: ReadonlyArray<string>,
19
- ): Promise<string | null> => {
19
+ ): Promise<{
20
+ readonly problem: string | null;
21
+ readonly occupied: ReadonlyArray<string>;
22
+ }> => {
20
23
  const inspected = await Promise.all(
21
24
  paths.map((path) => inspectSopsScalarDestination(consumer, rel, path)),
22
25
  );
23
26
  const blocked = inspected.find((result) => !result.ok);
24
- return blocked !== undefined && !blocked.ok ? blocked.problem : null;
27
+ return {
28
+ problem: blocked !== undefined && !blocked.ok ? blocked.problem : null,
29
+ occupied: paths.filter((_, index) => {
30
+ const result = inspected[index];
31
+ return result?.ok && result.state === 'scalar';
32
+ }),
33
+ };
25
34
  };
26
35
 
27
36
  export type ResolvedResourceFlags =
package/src/creds-add.ts CHANGED
@@ -69,6 +69,17 @@ const printSuccess = (input: {
69
69
  }
70
70
  };
71
71
 
72
+ const warnOccupiedDestinations = (
73
+ target: string,
74
+ paths: ReadonlyArray<string>,
75
+ ): void => {
76
+ for (const path of paths) {
77
+ console.error(
78
+ `standards creds: warning — ${target}:${path} already holds a value and will be overwritten; the old provider credential is not revoked by this write. Run \`standards creds plan\` to inspect unmanaged token candidates before retiring it.`,
79
+ );
80
+ }
81
+ };
82
+
72
83
  export const runCredsAddCloudflare = async (
73
84
  consumer: string,
74
85
  options: {
@@ -104,13 +115,9 @@ export const runCredsAddCloudflare = async (
104
115
  const format: DestinationFormat = options.s3 ? 's3' : 'bearer';
105
116
  const paths =
106
117
  format === 's3' ? s3PairPaths(context.dest.key) : [context.dest.key];
107
- const destinationProblem = await inspectDestinations(
108
- consumer,
109
- context.rel,
110
- paths,
111
- );
112
- if (destinationProblem !== null) {
113
- console.error(`standards creds: ${destinationProblem}`);
118
+ const destinations = await inspectDestinations(consumer, context.rel, paths);
119
+ if (destinations.problem !== null) {
120
+ console.error(`standards creds: ${destinations.problem}`);
114
121
  return false;
115
122
  }
116
123
  const encryptedKeys = await readEncryptedKeys(consumer, context.rel);
@@ -137,6 +144,7 @@ export const runCredsAddCloudflare = async (
137
144
  console.error(`standards creds: ${collisionProblem}`);
138
145
  return false;
139
146
  }
147
+ warnOccupiedDestinations(context.dest.target, destinations.occupied);
140
148
  const ttlDays = options.ttlDays ?? DEFAULT_TTL_DAYS;
141
149
  const expiresOn = cloudflareExpiresOn(Date.now() + ttlDays * DAY_MS);
142
150
  const created = await createAccountToken(account.accountId, account.token, {
@@ -120,7 +120,7 @@ export const listPermissionGroups = async (
120
120
  const response = await cfRequest(
121
121
  token,
122
122
  'GET',
123
- `/accounts/${accountId}/tokens/permission_groups?per_page=1000`,
123
+ `/accounts/${accountId}/tokens/permission_groups`,
124
124
  );
125
125
  if (!response.ok) {
126
126
  return response;
@@ -1,6 +1,6 @@
1
1
  // Cloudflare broker bootstrap. Cloudflare's API can create account-owned
2
2
  // tokens, but the very first token must come from the dashboard — this is the
3
- // one paste per account, ever. The flow tells the user exactly what to click
3
+ // one paste per account per machine. The flow tells the user exactly what to click
4
4
  // and which single permission to grant, takes the token hidden, verifies it
5
5
  // against the API before storing, and from then on every real credential is a
6
6
  // scoped, expiring account token minted by `standards creds add`.
@@ -109,7 +109,9 @@ export const runCredsLoginCloudflare = async (options: {
109
109
  return false;
110
110
  }
111
111
  const tokensUrl = `https://dash.cloudflare.com/${accountId}/api-tokens`;
112
- console.log('Create the bootstrap token (one time for this account):');
112
+ console.log(
113
+ 'Create the bootstrap token (one time for this account on this machine):',
114
+ );
113
115
  console.log(` 1. Open ${tokensUrl}`);
114
116
  console.log(' 2. Select Create Token');
115
117
  console.log(' 3. Find Create additional tokens and select Use template');
@@ -118,6 +120,9 @@ export const runCredsLoginCloudflare = async (options: {
118
120
  ' 5. Keep exactly one permission: Account / Account API Tokens / Edit',
119
121
  );
120
122
  console.log(' 6. Continue to summary, create the token, and copy the value');
123
+ console.log(
124
+ `Each machine needs its own ${BROKER_IDENTITY_NAME} token. Cloudflare allows duplicate names; leave an existing token with that name alone, and never reuse, roll, or revoke another machine's token.`,
125
+ );
121
126
  openInBrowser(tokensUrl);
122
127
  const token = await promptHidden('Paste the token (input is hidden): ');
123
128
  if (token.length === 0) {
@@ -64,22 +64,22 @@ const parseRepos = (
64
64
  }
65
65
  if (!raw.every((repo): repo is string => typeof repo === 'string')) {
66
66
  problems.push('poller config "repos" must be a string array');
67
- return [];
68
67
  }
68
+ const repos = raw.filter((repo): repo is string => typeof repo === 'string');
69
69
  if (raw.length === 0) {
70
70
  problems.push('poller config "repos" must list at least one repository');
71
71
  }
72
- for (const repo of raw) {
72
+ for (const repo of repos) {
73
73
  if (!REPO_PATTERN.test(repo)) {
74
74
  problems.push(
75
75
  `poller config "repos" entries must be "owner/repo": ${repo}`,
76
76
  );
77
77
  }
78
78
  }
79
- if (new Set(raw).size !== raw.length) {
79
+ if (new Set(repos).size !== repos.length) {
80
80
  problems.push('poller config "repos" entries must be unique');
81
81
  }
82
- return raw;
82
+ return repos;
83
83
  };
84
84
 
85
85
  const parsePositiveInteger = (
@@ -73,7 +73,6 @@ Description=Run the standards fix poller on an interval
73
73
  OnBootSec=2min
74
74
  OnUnitInactiveSec=${WORKER_INTERVAL_MINUTES}min
75
75
  AccuracySec=15s
76
- Persistent=true
77
76
 
78
77
  [Install]
79
78
  WantedBy=timers.target
@@ -95,7 +94,6 @@ Description=Check for newly queued standards poller requests
95
94
  OnBootSec=1min
96
95
  OnUnitInactiveSec=${ACKNOWLEDGEMENT_INTERVAL_MINUTES}min
97
96
  AccuracySec=15s
98
- Persistent=true
99
97
 
100
98
  [Install]
101
99
  WantedBy=timers.target
@@ -1,6 +1,7 @@
1
1
  import { existsSync } from 'node:fs';
2
2
  import { readdir } from 'node:fs/promises';
3
3
  import { isAbsolute, join } from 'node:path';
4
+ import { isContainedPath } from './contained-path';
4
5
  import { isRecord } from './github-settings-parse';
5
6
  import { readJsonFile } from './json-file';
6
7
  import { collectBunVersionProblems } from './structure-bun-version';
@@ -9,7 +10,6 @@ import {
9
10
  rootScriptExpectations,
10
11
  type StructureProfile,
11
12
  } from './structure-profile';
12
- import { collectWorkspaceReadmeProblems } from './structure-readme';
13
13
  import { filteredTurboAliasProblem, hasSafeCommands } from './structure-script';
14
14
  import { collectCiSecretsProblems } from './structure-secrets';
15
15
  import { inspectWorkspace, type Workspace } from './structure-workspace';
@@ -106,6 +106,12 @@ const loadWorkspace = async (
106
106
  if (!existsSync(path)) {
107
107
  return { workspace: null, problem: null };
108
108
  }
109
+ if (!isContainedPath(consumer, `${rel}/package.json`, 'file')) {
110
+ return {
111
+ workspace: null,
112
+ problem: `${rel}: package.json must be a contained regular file; symlinked paths are not allowed`,
113
+ };
114
+ }
109
115
  const manifest = await readJsonFile(path);
110
116
  if (manifest === null) {
111
117
  return {
@@ -174,16 +180,9 @@ export const collectStructureProblems = async (
174
180
  .map((ws) => ws.manifest.name)
175
181
  .filter((name): name is string => typeof name === 'string'),
176
182
  );
177
- const [inspections, readmeProblems] = await Promise.all([
178
- Promise.all(
179
- workspaces.map((ws) => inspectWorkspace(ws, workspaceNames, profile)),
180
- ),
181
- collectWorkspaceReadmeProblems(
182
- consumer,
183
- profile,
184
- workspaces.map((ws) => ws.rel),
185
- ),
186
- ]);
183
+ const inspections = await Promise.all(
184
+ workspaces.map((ws) => inspectWorkspace(ws, workspaceNames, profile)),
185
+ );
187
186
  const requireA11y = inspections.some((i) => i.hasA11ySuite);
188
187
  return [
189
188
  ...declaration.problems,
@@ -193,7 +192,6 @@ export const collectStructureProblems = async (
193
192
  ...collectBunVersionProblems(root),
194
193
  ...inspectRootScripts(root, profile, requireA11y),
195
194
  ...inspections.flatMap((i) => [...i.problems]),
196
- ...readmeProblems,
197
195
  ...secretsProblems,
198
196
  ];
199
197
  };
@@ -58,11 +58,15 @@ const containsA11ySuite = async (dir: string): Promise<boolean> => {
58
58
  return nested.includes(true);
59
59
  };
60
60
  const inspectScripts = (ws: Workspace): ReadonlyArray<string> =>
61
- WORKSPACE_SCRIPTS.flatMap(([name, command]) =>
62
- hasSafeCommand(scriptOf(ws.manifest, name), command)
63
- ? []
64
- : [`${ws.rel}: script "${name}" must run ${command}`],
65
- );
61
+ WORKSPACE_SCRIPTS.flatMap(([name, command]) => {
62
+ const script = scriptOf(ws.manifest, name);
63
+ const accepted =
64
+ hasSafeCommand(script, command) ||
65
+ (name === 'test' && hasSafeCommand(script, 'bun test --isolate'));
66
+ const expected =
67
+ name === 'test' ? 'bun test or bun test --isolate' : command;
68
+ return accepted ? [] : [`${ws.rel}: script "${name}" must run ${expected}`];
69
+ });
66
70
  const declaredDependencies = (manifest: Record<string, unknown>) =>
67
71
  DEPENDENCY_FIELDS.flatMap((field) => {
68
72
  const deps = manifest[field];
@@ -1,55 +0,0 @@
1
- import type { CloudflareToken, TokenPolicy } from './creds-cloudflare-api';
2
- import type { TokenCondition } from './creds-cloudflare-condition';
3
- import type { DestinationFormat } from './creds-r2';
4
-
5
- export type AccountToken = {
6
- readonly accountId: string;
7
- readonly token: CloudflareToken;
8
- };
9
-
10
- export type UnmanagedToken = {
11
- readonly accountId: string;
12
- readonly tokenId: string;
13
- readonly name: string;
14
- readonly status: string;
15
- };
16
-
17
- // A token the broker minted under another repository's name. That repository
18
- // normally reconciles it, so it is not unmanaged — the distinction matters
19
- // because it stops being true when that repository is renamed, transferred, or
20
- // deleted, and this account is then the only place the token is still visible.
21
- export type BrokeredElsewhereToken = UnmanagedToken & {
22
- readonly repo: string;
23
- };
24
-
25
- export type PlannedAction =
26
- | {
27
- readonly kind: 'revoke';
28
- readonly accountId: string;
29
- readonly tokenId: string;
30
- readonly name: string;
31
- readonly reason: string;
32
- }
33
- | {
34
- readonly kind: 'renew';
35
- readonly accountId: string;
36
- readonly tokenId: string;
37
- readonly name: string;
38
- readonly target: string;
39
- readonly key: string;
40
- readonly format: DestinationFormat;
41
- readonly policies: ReadonlyArray<TokenPolicy>;
42
- readonly condition: TokenCondition | null;
43
- readonly replacementExpiresOn: string;
44
- readonly reason: string;
45
- };
46
-
47
- export type CredsPlan = {
48
- readonly actions: ReadonlyArray<PlannedAction>;
49
- readonly findings: ReadonlyArray<string>;
50
- readonly healthy: number;
51
- // Tokens in the account the broker did not mint. Reported, never mutated.
52
- readonly unmanaged: ReadonlyArray<UnmanagedToken>;
53
- // Tokens the broker minted for another repository. Reported, never mutated.
54
- readonly brokeredElsewhere: ReadonlyArray<BrokeredElsewhereToken>;
55
- };
@@ -1,85 +0,0 @@
1
- import { lstat, readFile } from 'node:fs/promises';
2
- import { join } from 'node:path';
3
- import { isContainedPath } from './contained-path';
4
- import { readJsonFile } from './json-file';
5
- import type { StructureProfile } from './structure-profile';
6
-
7
- const SYNC_MANIFEST = 'sync-standards.json';
8
- const TRAILING_SLASHES = /\/+$/u;
9
- const MANIFEST_REQUIREMENT = `${SYNC_MANIFEST}: must contain a JSON object with a "paths" array of strings; the structure gate reads it to tell canonical workspaces from repo-owned ones`;
10
- const MANIFEST_CONTAINMENT = `${SYNC_MANIFEST}: must be a contained regular file; symlinked paths are not allowed`;
11
-
12
- type CanonicalRels = {
13
- readonly rels: ReadonlySet<string>;
14
- readonly problem: string | null;
15
- };
16
-
17
- // Canonical workspaces are synced from the standards template and cannot be
18
- // fixed in a consumer checkout, so the consumer profile exempts them from
19
- // repo-owned documentation rules. The source repository owns those workspaces,
20
- // so the source profile exempts nothing.
21
- const canonicalWorkspaceRels = async (
22
- consumer: string,
23
- profile: StructureProfile,
24
- ): Promise<CanonicalRels> => {
25
- if (profile === 'source') {
26
- return { rels: new Set(), problem: null };
27
- }
28
- const manifestPath = join(consumer, SYNC_MANIFEST);
29
- const manifestInfo = await lstat(manifestPath).catch(() => null);
30
- if (
31
- manifestInfo !== null &&
32
- !isContainedPath(consumer, SYNC_MANIFEST, 'file')
33
- ) {
34
- return { rels: new Set(), problem: MANIFEST_CONTAINMENT };
35
- }
36
- const manifest = await readJsonFile(manifestPath);
37
- const paths = manifest?.paths;
38
- if (
39
- !(
40
- Array.isArray(paths) &&
41
- paths.every((path): path is string => typeof path === 'string')
42
- )
43
- ) {
44
- return { rels: new Set(), problem: MANIFEST_REQUIREMENT };
45
- }
46
- return {
47
- rels: new Set(paths.map((path) => path.replace(TRAILING_SLASHES, ''))),
48
- problem: null,
49
- };
50
- };
51
-
52
- const readmeProblem = async (
53
- consumer: string,
54
- rel: string,
55
- ): Promise<string | null> => {
56
- const readmeRel = `${rel}/README.md`;
57
- const readmePath = join(consumer, readmeRel);
58
- const info = await lstat(readmePath).catch(() => null);
59
- if (info !== null && !isContainedPath(consumer, readmeRel, 'file')) {
60
- return `${rel}: README.md must be a contained regular file; symlinked paths are not allowed`;
61
- }
62
- const raw = await readFile(readmePath, 'utf8').catch(() => null);
63
- return raw !== null && raw.trim() !== ''
64
- ? null
65
- : `${rel}: repo-owned workspace must have a non-empty README.md`;
66
- };
67
-
68
- export const collectWorkspaceReadmeProblems = async (
69
- consumer: string,
70
- profile: StructureProfile,
71
- rels: ReadonlyArray<string>,
72
- ): Promise<ReadonlyArray<string>> => {
73
- const canonical = await canonicalWorkspaceRels(consumer, profile);
74
- if (canonical.problem !== null) {
75
- // Without the manifest the gate cannot tell canonical workspaces from
76
- // repo-owned ones, so it fails on the manifest instead of guessing.
77
- return [canonical.problem];
78
- }
79
- const problems = await Promise.all(
80
- rels
81
- .filter((rel) => !canonical.rels.has(rel))
82
- .map((rel) => readmeProblem(consumer, rel)),
83
- );
84
- return problems.filter((problem): problem is string => problem !== null);
85
- };