@davidvornholt/standards 0.26.2 → 0.26.4
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 +6 -0
- package/package.json +2 -2
- package/src/cli.ts +27 -1
- package/src/creds-add-collision.ts +2 -2
- package/src/creds-add-github.ts +2 -2
- package/src/creds-add-preflight.ts +11 -2
- package/src/creds-add.ts +15 -7
- package/src/creds-cloudflare.ts +1 -1
- package/src/creds-login-cloudflare.ts +7 -2
- package/src/poller-config.ts +4 -4
- package/src/poller-units.ts +0 -2
- package/src/source-text.ts +98 -0
- package/src/structure-profile.ts +2 -0
- package/src/structure-workspace.ts +9 -5
- package/src/creds-plan-types.ts +0 -55
package/README.md
CHANGED
|
@@ -263,3 +263,9 @@ 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.
|
|
268
|
+
|
|
269
|
+
## Source text validation
|
|
270
|
+
|
|
271
|
+
`bun standards source-text` checks tracked source, configuration, and documentation files as UTF-8 text, regardless of Git’s binary classification. `bun standards check` includes this gate. Raw Unicode control characters are rejected except tab and LF; write control characters in fixtures and composite keys using escaped representations such as `\0` or `\u0000`. Binary asset formats and untracked files are outside this gate. Missing tracked source files and source symlinks fail validation.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@davidvornholt/standards",
|
|
3
|
-
"version": "0.26.
|
|
3
|
+
"version": "0.26.4",
|
|
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",
|
|
@@ -163,6 +162,7 @@
|
|
|
163
162
|
"src/screenshots-publish.ts",
|
|
164
163
|
"src/screenshots-upload.ts",
|
|
165
164
|
"src/sops-exec.ts",
|
|
165
|
+
"src/source-text.ts",
|
|
166
166
|
"src/structure-check.ts",
|
|
167
167
|
"src/structure-bun-version.ts",
|
|
168
168
|
"src/structure-profile.ts",
|
package/src/cli.ts
CHANGED
|
@@ -44,6 +44,7 @@ import {
|
|
|
44
44
|
} from './managed-path-ownership';
|
|
45
45
|
import { runPollerCommand } from './poller-commands';
|
|
46
46
|
import { runScreenshotsCommand } from './screenshots-commands';
|
|
47
|
+
import { collectSourceTextProblems } from './source-text';
|
|
47
48
|
import { collectStructureProblems } from './structure-check';
|
|
48
49
|
import type { StructureProfile } from './structure-profile';
|
|
49
50
|
import { hasSafeCommand } from './structure-script';
|
|
@@ -92,6 +93,7 @@ type Command =
|
|
|
92
93
|
| 'help'
|
|
93
94
|
| 'init'
|
|
94
95
|
| 'poller'
|
|
96
|
+
| 'source-text'
|
|
95
97
|
| 'structure'
|
|
96
98
|
| 'sync';
|
|
97
99
|
|
|
@@ -969,6 +971,7 @@ Commands:
|
|
|
969
971
|
sync Mirror canonical files from upstream, regenerate the composed Dependabot config, and rewrite the lock
|
|
970
972
|
check Verify canonical files, extension seams, monorepo structure, and GitHub settings
|
|
971
973
|
doctor Validate extension seams only
|
|
974
|
+
source-text Reject raw control characters in tracked source text
|
|
972
975
|
structure Validate monorepo structure rules only
|
|
973
976
|
dependabot Verify (--check) or regenerate (--write) the composed .github/dependabot.yml
|
|
974
977
|
dev-env Compose each workspace's generated .env.local from its three dev layers and authorized broker-owned S3 pair references
|
|
@@ -1002,6 +1005,7 @@ const commandFromArg = (arg: string): Command => {
|
|
|
1002
1005
|
arg === 'help' ||
|
|
1003
1006
|
arg === 'init' ||
|
|
1004
1007
|
arg === 'poller' ||
|
|
1008
|
+
arg === 'source-text' ||
|
|
1005
1009
|
arg === 'structure' ||
|
|
1006
1010
|
arg === 'sync'
|
|
1007
1011
|
) {
|
|
@@ -1209,10 +1213,24 @@ const runStructure = async (
|
|
|
1209
1213
|
return true;
|
|
1210
1214
|
};
|
|
1211
1215
|
|
|
1216
|
+
const runSourceTextCheck = (consumer: string): boolean => {
|
|
1217
|
+
const problems = collectSourceTextProblems(consumer);
|
|
1218
|
+
if (problems.length > 0) {
|
|
1219
|
+
console.error(`standards source-text: ${problems.length} problem(s):`);
|
|
1220
|
+
console.error(problems.map((problem) => ` - ${problem}`).join('\n'));
|
|
1221
|
+
return false;
|
|
1222
|
+
}
|
|
1223
|
+
console.log(
|
|
1224
|
+
'standards source-text: tracked source text contains no forbidden control characters',
|
|
1225
|
+
);
|
|
1226
|
+
return true;
|
|
1227
|
+
};
|
|
1228
|
+
|
|
1212
1229
|
const runCheckCommand = async (consumer: string): Promise<boolean> => {
|
|
1213
1230
|
const driftIsClean = await runCheck(consumer);
|
|
1214
1231
|
const integrationIsValid = await runDoctor(consumer);
|
|
1215
1232
|
const structureIsValid = await runStructure(consumer, 'consumer');
|
|
1233
|
+
const sourceTextIsValid = runSourceTextCheck(consumer);
|
|
1216
1234
|
// The GitHub gate activates with the synced declaration and then fails
|
|
1217
1235
|
// closed: once .github/settings.json exists, an unreachable API or an
|
|
1218
1236
|
// unreadable origin is a failure, not a skip.
|
|
@@ -1220,7 +1238,11 @@ const runCheckCommand = async (consumer: string): Promise<boolean> => {
|
|
|
1220
1238
|
? await runGithubCheckGate(consumer)
|
|
1221
1239
|
: true;
|
|
1222
1240
|
return (
|
|
1223
|
-
driftIsClean &&
|
|
1241
|
+
driftIsClean &&
|
|
1242
|
+
integrationIsValid &&
|
|
1243
|
+
structureIsValid &&
|
|
1244
|
+
sourceTextIsValid &&
|
|
1245
|
+
githubIsConverged
|
|
1224
1246
|
);
|
|
1225
1247
|
};
|
|
1226
1248
|
|
|
@@ -1296,6 +1318,7 @@ const runGateCommand = (
|
|
|
1296
1318
|
| 'dev-env'
|
|
1297
1319
|
| 'doctor'
|
|
1298
1320
|
| 'github'
|
|
1321
|
+
| 'source-text'
|
|
1299
1322
|
| 'structure',
|
|
1300
1323
|
consumer: string,
|
|
1301
1324
|
apply: boolean,
|
|
@@ -1313,6 +1336,9 @@ const runGateCommand = (
|
|
|
1313
1336
|
if (command === 'doctor') {
|
|
1314
1337
|
return runDoctor(consumer);
|
|
1315
1338
|
}
|
|
1339
|
+
if (command === 'source-text') {
|
|
1340
|
+
return Promise.resolve(runSourceTextCheck(consumer));
|
|
1341
|
+
}
|
|
1316
1342
|
if (command === 'structure') {
|
|
1317
1343
|
return runStructure(consumer, profile);
|
|
1318
1344
|
}
|
|
@@ -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(({
|
|
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
|
-
? [`${
|
|
38
|
+
? [`${ref.target}:${ref.key}`]
|
|
39
39
|
: [];
|
|
40
40
|
})
|
|
41
41
|
: [],
|
package/src/creds-add-github.ts
CHANGED
|
@@ -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<
|
|
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
|
|
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
|
|
108
|
-
|
|
109
|
-
|
|
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, {
|
package/src/creds-cloudflare.ts
CHANGED
|
@@ -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
|
|
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
|
|
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(
|
|
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) {
|
package/src/poller-config.ts
CHANGED
|
@@ -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
|
|
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(
|
|
79
|
+
if (new Set(repos).size !== repos.length) {
|
|
80
80
|
problems.push('poller config "repos" entries must be unique');
|
|
81
81
|
}
|
|
82
|
-
return
|
|
82
|
+
return repos;
|
|
83
83
|
};
|
|
84
84
|
|
|
85
85
|
const parsePositiveInteger = (
|
package/src/poller-units.ts
CHANGED
|
@@ -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
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { execFileSync } from 'node:child_process';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import { basename, extname, join } from 'node:path';
|
|
4
|
+
import { isContainedPath } from './contained-path';
|
|
5
|
+
|
|
6
|
+
const sourceExtensions = new Set(
|
|
7
|
+
'.ts,.tsx,.mts,.cts,.js,.jsx,.mjs,.cjs,.json,.jsonc,.yaml,.yml,.toml,.nix,.sh,.bash,.zsh,.rb,.py,.css,.scss,.html,.xml,.svg,.md,.mdx,.sql,.just'.split(
|
|
8
|
+
',',
|
|
9
|
+
),
|
|
10
|
+
);
|
|
11
|
+
const sourceFileNames = new Set([
|
|
12
|
+
'Dockerfile',
|
|
13
|
+
'Containerfile',
|
|
14
|
+
'Makefile',
|
|
15
|
+
'justfile',
|
|
16
|
+
'.envrc',
|
|
17
|
+
'.gitignore',
|
|
18
|
+
'.gitattributes',
|
|
19
|
+
'.dockerignore',
|
|
20
|
+
'.editorconfig',
|
|
21
|
+
'.npmrc',
|
|
22
|
+
]);
|
|
23
|
+
const dotenvFileName = /^\.env(?:\..*)?$/u;
|
|
24
|
+
const forbiddenControl = /[^\P{Cc}\t\n]/u;
|
|
25
|
+
const gitOutputLimit = 16_777_216;
|
|
26
|
+
const hexadecimalRadix = 16;
|
|
27
|
+
const minimumCodePointWidth = 4;
|
|
28
|
+
|
|
29
|
+
export const inspectSourceText = (
|
|
30
|
+
path: string,
|
|
31
|
+
bytes: Uint8Array,
|
|
32
|
+
): string | null => {
|
|
33
|
+
let text: string;
|
|
34
|
+
try {
|
|
35
|
+
text = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
36
|
+
} catch {
|
|
37
|
+
return `${JSON.stringify(path)}: source text must be valid UTF-8`;
|
|
38
|
+
}
|
|
39
|
+
const match = forbiddenControl.exec(text);
|
|
40
|
+
if (!match) {
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
const point = match[0]
|
|
44
|
+
.codePointAt(0)
|
|
45
|
+
?.toString(hexadecimalRadix)
|
|
46
|
+
.toUpperCase()
|
|
47
|
+
.padStart(minimumCodePointWidth, '0');
|
|
48
|
+
const line = text.slice(0, match.index).split('\n').length;
|
|
49
|
+
return `${JSON.stringify(path)}:${line}: raw control character U+${point}; use an escaped representation (only tab and LF are allowed)`;
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
export const collectSourceTextProblems = (
|
|
53
|
+
root: string,
|
|
54
|
+
): ReadonlyArray<string> => {
|
|
55
|
+
let tracked: string;
|
|
56
|
+
try {
|
|
57
|
+
tracked = execFileSync('git', ['ls-files', '--cached', '-z'], {
|
|
58
|
+
cwd: root,
|
|
59
|
+
encoding: 'utf8',
|
|
60
|
+
maxBuffer: gitOutputLimit,
|
|
61
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
62
|
+
});
|
|
63
|
+
} catch {
|
|
64
|
+
return [
|
|
65
|
+
'Cannot enumerate tracked source files; run the gate from a readable Git working tree.',
|
|
66
|
+
];
|
|
67
|
+
}
|
|
68
|
+
const problems: Array<string> = [];
|
|
69
|
+
const paths = new Set(
|
|
70
|
+
tracked
|
|
71
|
+
.split('\0')
|
|
72
|
+
.filter(
|
|
73
|
+
(path) =>
|
|
74
|
+
sourceExtensions.has(extname(path)) ||
|
|
75
|
+
sourceFileNames.has(basename(path)) ||
|
|
76
|
+
dotenvFileName.test(basename(path)),
|
|
77
|
+
),
|
|
78
|
+
);
|
|
79
|
+
for (const path of paths) {
|
|
80
|
+
if (isContainedPath(root, path, 'file')) {
|
|
81
|
+
try {
|
|
82
|
+
const problem = inspectSourceText(path, readFileSync(join(root, path)));
|
|
83
|
+
if (problem !== null) {
|
|
84
|
+
problems.push(problem);
|
|
85
|
+
}
|
|
86
|
+
} catch {
|
|
87
|
+
problems.push(
|
|
88
|
+
`${JSON.stringify(path)}: cannot read tracked source text`,
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
problems.push(
|
|
93
|
+
`${JSON.stringify(path)}: tracked source must be a contained regular file`,
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return problems;
|
|
98
|
+
};
|
package/src/structure-profile.ts
CHANGED
|
@@ -31,6 +31,7 @@ const SOURCE_ROOT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
|
|
|
31
31
|
name: 'check',
|
|
32
32
|
commands: [
|
|
33
33
|
`${SOURCE_CLI} structure --profile source`,
|
|
34
|
+
`${SOURCE_CLI} source-text`,
|
|
34
35
|
`${SOURCE_CLI} dependabot --check`,
|
|
35
36
|
`${SOURCE_CLI} github --check`,
|
|
36
37
|
'turbo run lint check-types test --output-logs=errors-only',
|
|
@@ -41,6 +42,7 @@ const SOURCE_ROOT_EXPECTATIONS: ReadonlyArray<RootScriptExpectation> = [
|
|
|
41
42
|
name: 'check:fix',
|
|
42
43
|
commands: [
|
|
43
44
|
`${SOURCE_CLI} structure --profile source`,
|
|
45
|
+
`${SOURCE_CLI} source-text`,
|
|
44
46
|
`${SOURCE_CLI} dependabot --write`,
|
|
45
47
|
`${SOURCE_CLI} github --check`,
|
|
46
48
|
'turbo run lint:fix check-types test --output-logs=errors-only',
|
|
@@ -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
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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];
|
package/src/creds-plan-types.ts
DELETED
|
@@ -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
|
-
};
|