@starci/hfs 4.7.11 → 4.7.12
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/CHANGELOG.md +3 -0
- package/package.json +1 -1
- package/runtime/engine/digest.mjs +16 -0
- package/runtime/knowledge/hfs/canon-pins.yaml +4 -4
- package/runtime/packages/cli/src/catalog.generated.mjs +7 -4
- package/runtime/scripts/api/fs/is-link-like.mjs +1 -0
- package/runtime/scripts/api/sops/lib.mjs +5 -1
- package/runtime/scripts/hfs/architecture/i18n-keys.mjs +1 -1
- package/runtime/scripts/hfs/architecture/tiers.mjs +1 -2
- package/runtime/scripts/hfs/db-plan-verb.mjs +4 -5
- package/runtime/scripts/hfs/rule-catalog.mjs +3 -2
- package/runtime/scripts/hfs/rules/deploy-scan.mjs +1 -1
- package/runtime/scripts/hfs/rules/doc-fresh.mjs +1 -1
- package/runtime/scripts/hfs/rules/frontend-tree.mjs +1 -1
- package/runtime/scripts/hfs/rules/release-provenance.mjs +1 -1
- package/runtime/scripts/hfs/rules/script-hygiene.mjs +1 -1
- package/runtime/scripts/hfs/rules/stack-tooling.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-release.mjs +1 -1
- package/runtime/scripts/hfs/rules/stacks-swarm.mjs +1 -1
- package/runtime/scripts/hfs/rules/supply-chain.mjs +1 -0
- package/runtime/scripts/hfs/rules/workflow-contexts.mjs +1 -1
- package/runtime/scripts/hfs/secret-bootstrap-crypto.mjs +1 -1
- package/runtime/scripts/lib/path-key.mjs +2 -0
- package/runtime/scripts/lib/secret-patterns.mjs +1 -1
- package/runtime/scripts/lib/ts-analysis.mjs +8 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 4.7.12 - 2026-10-19
|
|
4
|
+
- Fixed: bundled source analysis uses explicit nullable contracts, unary callback arguments and grouped regular-expression alternatives while preserving refusal paths; canon pins follow the matching package cohort.
|
|
5
|
+
|
|
3
6
|
## 4.7.11 - 2026-10-19
|
|
4
7
|
- Changed: app release and Sonar adapters use native target binding, with local database migration and Docker transport preserving the declared stack target.
|
|
5
8
|
|
package/package.json
CHANGED
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// digest.mjs — the one SHA-256 the runtime hashes with. A leaf: every file's integrity digest,
|
|
2
|
+
// every ledger input ref and every stamped asset is this function, so a byte stream and a file
|
|
3
|
+
// never disagree about what their digest is.
|
|
4
|
+
import { createHash } from 'node:crypto';
|
|
5
|
+
import fs from 'node:fs';
|
|
6
|
+
|
|
7
|
+
/** The lowercase hex SHA-256 of `value` (string, Buffer or TypedArray). */
|
|
8
|
+
export const sha256 = (value) => createHash('sha256').update(value).digest('hex');
|
|
9
|
+
/** The SHA-256 of the file's exact bytes; throws when the file cannot be read. */
|
|
10
|
+
export function sha256File(file){
|
|
11
|
+
// A numeric descriptor belongs to the caller; positional reads preserve its offset.
|
|
12
|
+
const owned=typeof file!=='number',fd=owned?fs.openSync(file,'r'):file;
|
|
13
|
+
const hash=createHash('sha256'),buffer=Buffer.alloc(1024*1024);
|
|
14
|
+
try{let at=0,n;while((n=fs.readSync(fd,buffer,0,buffer.length,at))>0){hash.update(buffer.subarray(0,n));at+=n;}return hash.digest('hex');}
|
|
15
|
+
finally{if(owned)fs.closeSync(fd);}
|
|
16
|
+
}
|
|
@@ -29,14 +29,14 @@ pins:
|
|
|
29
29
|
source: packages/grammar/package.json
|
|
30
30
|
why: 'the grammar front ends read: the brand layer sets `--font-sans` and `--font-mono` and the grammar reads them; it carries the Input tel kind, the copyable and read-only Input, the IconButton disclosure props and count badge, ChatBubble, FillViewport, Textarea mentions and Enter-to-send, the leading PrimaryRailLayout rail, the plate-less Image, ListRow, the soft Alert, Progress steps, the floating primary IconButton, FlowGraph, the STATE-8 loading views and the bordered selectable TagGroup chips. The runtime source version wins for a @starci package.'
|
|
31
31
|
'@starci/eslint-canon-be':
|
|
32
|
-
version: 3.4.
|
|
32
|
+
version: 3.4.18
|
|
33
33
|
group: starci
|
|
34
34
|
install: registry
|
|
35
35
|
side: be
|
|
36
36
|
source: packages/eslint/be/package.json
|
|
37
37
|
why: 'the back-end ESLint canon: `loadHfs(import.meta.url)` of be/eslint.config.mjs finds the app-root hfs.json (kind app) and gives every linted file the view of the be side, and the project graph is built per side; its bundled runtime copies (slots, canon-pins, failure codes, the architecture machine) put .starcistacks and .sops.yaml at the app root; `starciBeConfig({ hfs: loadHfs(import.meta.url) })` is the typed factory and the BE-CONVENTION laws are its rules.'
|
|
38
38
|
'@starci/eslint-canon-fe':
|
|
39
|
-
version: 8.4.
|
|
39
|
+
version: 8.4.18
|
|
40
40
|
group: starci
|
|
41
41
|
install: registry
|
|
42
42
|
side: fe
|
|
@@ -76,14 +76,14 @@ pins:
|
|
|
76
76
|
source: packages/test-world/package.json
|
|
77
77
|
why: 'the shared e2e library of every back end (R47, R48), a devDependency of every back end: the warm stack behind toxiproxy, the network-edge fakes, the Nest boot, the typed useTestWorld handle, useSandbox for contract specs and the outage lock. Kafka is real own infra (the one digest-pinned apache/kafka KRaft image, a broker listener and proxy per slot, slot-prefixed topics, consumer groups and client ids) and Postgres is schema-per-context (a schema and a login role per context in a shared database). Each worker has its own data slot (<ns>_w<k>) with its own databases, Keycloak realm, leased Redis DB, bucket/collection/topic prefixes, fakes host, toxiproxy proxies and outage lock; a run provisions min(jest workers, workers cap, default 2) slots, and state file protocol 2 pairs exactly with @starci/jest-preset (TEST_WORLD_PAIR_MISMATCH otherwise). A modules world boots real peer apps beside its modules, world.apps.<name>.during(fn) is the outage of a peer app, w.keycloak.clientSecret(client) answers the run-generated secret of a confidential realm client, world.keycloak.events/sessions read the realm user events and live sessions, world.infra.postgresql.connection(name) takes one database connection down while the others serve, and world.resolve and scope.resolve take any Nest token. Every path a declaration names (`stack`, seeds, the realm, Dockerfiles) resolves from the app root (the directory of hfs.json); the globalSetup registers the path aliases as TypeScript resolves them. Its CLI main is exported at ./cli for starci app stack.'
|
|
78
78
|
'@starci/cli':
|
|
79
|
-
version: 1.7.
|
|
79
|
+
version: 1.7.12
|
|
80
80
|
group: starci
|
|
81
81
|
install: registry
|
|
82
82
|
side: both
|
|
83
83
|
source: packages/cli/package.json
|
|
84
84
|
why: 'the only StarCi command package; product repositories install it for starci app, while @starci/hfs remains its pinned transitive implementation.'
|
|
85
85
|
'@starci/hfs':
|
|
86
|
-
version: 4.7.
|
|
86
|
+
version: 4.7.12
|
|
87
87
|
group: starci
|
|
88
88
|
install: registry
|
|
89
89
|
side: both
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// GENERATED by scripts/cli/gen-catalog.mjs — do not edit
|
|
2
2
|
export const CATALOG = {
|
|
3
3
|
"schema": "starci/cli-catalog@1",
|
|
4
|
-
"hash": "
|
|
4
|
+
"hash": "8913c2eedad972f96410ed412622ca511f32f9a4f2b5fb8104b745e310007ac5",
|
|
5
5
|
"global": [
|
|
6
6
|
{
|
|
7
7
|
"name": "json",
|
|
@@ -10894,7 +10894,8 @@ export const CATALOG = {
|
|
|
10894
10894
|
"starci supabase start --cwd <app>"
|
|
10895
10895
|
],
|
|
10896
10896
|
"editions": [
|
|
10897
|
-
"full"
|
|
10897
|
+
"full",
|
|
10898
|
+
"lite"
|
|
10898
10899
|
],
|
|
10899
10900
|
"impl": {
|
|
10900
10901
|
"module": "scripts/machine/supabase-stack.mjs",
|
|
@@ -10931,7 +10932,8 @@ export const CATALOG = {
|
|
|
10931
10932
|
"starci supabase status --cwd <app> --json"
|
|
10932
10933
|
],
|
|
10933
10934
|
"editions": [
|
|
10934
|
-
"full"
|
|
10935
|
+
"full",
|
|
10936
|
+
"lite"
|
|
10935
10937
|
],
|
|
10936
10938
|
"impl": {
|
|
10937
10939
|
"module": "scripts/machine/supabase-stack.mjs",
|
|
@@ -10973,7 +10975,8 @@ export const CATALOG = {
|
|
|
10973
10975
|
"starci supabase stop --cwd <app> --no-backup"
|
|
10974
10976
|
],
|
|
10975
10977
|
"editions": [
|
|
10976
|
-
"full"
|
|
10978
|
+
"full",
|
|
10979
|
+
"lite"
|
|
10977
10980
|
],
|
|
10978
10981
|
"impl": {
|
|
10979
10982
|
"module": "scripts/machine/supabase-stack.mjs",
|
|
@@ -15,6 +15,7 @@ import { realpathOr } from '../../lib/fs-kind.mjs';
|
|
|
15
15
|
export function isLinkLike(p, { parentReal = null, stat = null } = {}) {
|
|
16
16
|
let st = stat;
|
|
17
17
|
if (!st) { try { st = fs.lstatSync(p); } catch { return false; } }
|
|
18
|
+
if (!st) return false;
|
|
18
19
|
if (st.isSymbolicLink()) return true;
|
|
19
20
|
if (!st.isDirectory()) return false;
|
|
20
21
|
try { fs.readlinkSync(p); return true; } catch { /* not a link readlink can read */ }
|
|
@@ -104,7 +104,11 @@ export const SELECTED_AGE_VERSIONS = new Set(['1.2.1', '1.3.1']);
|
|
|
104
104
|
const IDENTITY_TIMEOUT_MS = 20_000;
|
|
105
105
|
const CONTROLLED_ENV = new Set(['SOPS_AGE_KEY', 'SOPS_AGE_KEY_FILE', 'SOPS_AGE_KEY_CMD', 'SOPS_AGE_SSH_PRIVATE_KEY_FILE', 'SOPS_AGE_SSH_PRIVATE_KEY_CMD', 'SOPS_AGE_RECIPIENT', 'SOPS_KEYSERVICE', 'SOPS_ENABLE_LOCAL_KEYSERVICE', 'SOPS_DECRYPTION_ORDER', 'SOPS_KMS_ARN', 'SOPS_GCP_KMS_IDS', 'SOPS_HUAWEICLOUD_KMS_IDS', 'SOPS_AZURE_KEYVAULT_URLS', 'SOPS_VAULT_URIS', 'SOPS_PGP_FP', 'SOPS_AGE_RECIPIENTS', 'HOME', 'USERPROFILE', 'APPDATA', 'XDG_CONFIG_HOME']);
|
|
106
106
|
|
|
107
|
-
/**
|
|
107
|
+
/**
|
|
108
|
+
* Construct only the child map; empty desktop roots make Go return directory errors, not fallback paths.
|
|
109
|
+
* @param {object} env The admitted caller environment.
|
|
110
|
+
* @param {string|null} inlineName Selected inline identity name, or null to omit every identity.
|
|
111
|
+
*/
|
|
108
112
|
function isolatedSopsEnv(env, inlineName = 'SOPS_AGE_KEY') {
|
|
109
113
|
const out = Object.fromEntries(Object.entries(env).filter(([name]) => !CONTROLLED_ENV.has(name.toUpperCase())));
|
|
110
114
|
out.HOME = ''; out.USERPROFILE = ''; out.APPDATA = ''; out.XDG_CONFIG_HOME = '';
|
|
@@ -65,7 +65,7 @@ function templatePatternOf(node) {
|
|
|
65
65
|
return [node.head.text, ...node.templateSpans.map(span => span.literal.text)].join('\u0000').split('.').map(segment => {
|
|
66
66
|
if (!segment.includes('\u0000')) return segment;
|
|
67
67
|
const parts = segment.split('\u0000');
|
|
68
|
-
return parts.every(part => part === '') ? '*' : new RegExp(`^${parts.map(escapeRegExp).join('.*')}$`, 'su');
|
|
68
|
+
return parts.every(part => part === '') ? '*' : new RegExp(`^${parts.map((part) => escapeRegExp(part)).join('.*')}$`, 'su');
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
71
|
|
|
@@ -72,8 +72,7 @@ function stronglyConnected(adjacency) {
|
|
|
72
72
|
for (const start of adjacency.keys()) {
|
|
73
73
|
if (state.index.has(start)) continue;
|
|
74
74
|
startTraversal(state, adjacency, start);
|
|
75
|
-
|
|
76
|
-
const frame = state.work.at(-1);
|
|
75
|
+
for (let frame = state.work.at(-1); frame; frame = state.work.at(-1)) {
|
|
77
76
|
const next = frame.iterator.next();
|
|
78
77
|
if (next.done) {
|
|
79
78
|
finishFrame(state, frame);
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// app db plan: compare migration-owned state without fetching commands or Vault values.
|
|
2
2
|
import fs from 'node:fs';
|
|
3
3
|
import path from 'node:path';
|
|
4
|
-
import {
|
|
4
|
+
import { sha256 } from '../../engine/digest.mjs';
|
|
5
5
|
import pg from 'libpg-query';
|
|
6
6
|
import { parse as parseToml } from 'smol-toml';
|
|
7
7
|
import { invocationDir } from '../lib/roots.mjs';
|
|
@@ -19,12 +19,11 @@ const PRESENCE_SQL = `select
|
|
|
19
19
|
pg_catalog.to_regclass('vault.secrets') is not null as vault;`;
|
|
20
20
|
const INVENTORIES = {
|
|
21
21
|
migrations: 'select version from supabase_migrations.schema_migrations order by version;',
|
|
22
|
-
cron: 'select jobname as name, schedule, pg_catalog.
|
|
22
|
+
cron: 'select jobname as name, schedule, pg_catalog.encode(pg_catalog.sha256(pg_catalog.convert_to(command, \'UTF8\')), \'hex\') as "commandHash", active from cron.job order by jobid;',
|
|
23
23
|
queues: 'select queue_name as name from pgmq.meta order by queue_name;',
|
|
24
24
|
vault: 'select name from vault.secrets order by name;',
|
|
25
25
|
};
|
|
26
26
|
const names = (nodes) => sqlStringParts(nodes).join('.');
|
|
27
|
-
const hash = (command) => createHash('md5').update(command).digest('hex');
|
|
28
27
|
const OWNERSHIP_CALLS = new Set(['pgmq.create', 'pgmq.drop_queue', 'cron.schedule', 'cron.unschedule', 'cron.schedule_in_database', 'cron.alter_job']);
|
|
29
28
|
|
|
30
29
|
const parseSql = sqlParser(pg, () => { throw new DbVerbError('migration SQL could not be inventoried safely'); });
|
|
@@ -45,7 +44,7 @@ function inventoryCall(state, call) {
|
|
|
45
44
|
const job = requiredLiteral(call, 0, ['job_name']);
|
|
46
45
|
const schedule = requiredLiteral(call, 1, ['schedule']);
|
|
47
46
|
const command = requiredLiteral(call, 2, ['command']);
|
|
48
|
-
state.cron.set(job, { schedule, commandHash:
|
|
47
|
+
state.cron.set(job, { schedule, commandHash: sha256(command), active: true });
|
|
49
48
|
} else if (name === 'cron.unschedule') {
|
|
50
49
|
state.cron.delete(requiredLiteral(call, 0, ['job_name']));
|
|
51
50
|
} else if (name === 'cron.schedule_in_database' || name === 'cron.alter_job') {
|
|
@@ -224,7 +223,7 @@ function compare(expected, actual, kind) {
|
|
|
224
223
|
for (const row of actual) {
|
|
225
224
|
const key = kind === 'migrations' ? row.version : row.name;
|
|
226
225
|
if (key !== null && (typeof key !== 'string' || key.length === 0)) throw new DbVerbError('Supabase returned an invalid inventory identifier');
|
|
227
|
-
if (kind === 'cron' && (typeof row.schedule !== 'string' || !/^[a-f0-9]{
|
|
226
|
+
if (kind === 'cron' && (typeof row.schedule !== 'string' || !/^[a-f0-9]{64}$/.test(row.commandHash ?? '') || typeof row.active !== 'boolean')) throw new DbVerbError('Supabase returned invalid cron metadata');
|
|
228
227
|
if (!unmatched.delete(key)) unexpected += 1;
|
|
229
228
|
else if (kind === 'cron') {
|
|
230
229
|
const desired = expected.get(key);
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { skillRoot } from '../../engine/runtime-root.mjs';
|
|
5
5
|
import { parseYamlCached } from '../lib/yaml-cached.mjs';
|
|
6
6
|
import { jsonClone } from '../lib/json-clone.mjs';
|
|
7
|
+
import { byCodeUnit } from '../lib/list.mjs';
|
|
7
8
|
import { isPlainObject } from '../../engine/plain-object.mjs';
|
|
8
9
|
import { SEMVER } from './manifest-shape.mjs';
|
|
9
10
|
import { enforcerJudgedInEdition, judgedInEdition, ruleEditionProblems } from './edition-slots.mjs';
|
|
@@ -39,7 +40,7 @@ function relativePathProblem(value, label, bad, anchor = false) {
|
|
|
39
40
|
const parts = typeof value === 'string' ? value.split('#') : [];
|
|
40
41
|
const file = parts[0];
|
|
41
42
|
const valid = typeof value === 'string' && value.trim() === value && value.length > 0
|
|
42
|
-
&& !/[\r\n
|
|
43
|
+
&& !value.includes('\0') && !/[\r\n\\]/.test(value) && !/^(?:\/|[a-z][a-z0-9+.-]*:)/i.test(value) && !file.includes(':')
|
|
43
44
|
&& !file.split('/').some((part) => ['', '.', '..'].includes(part))
|
|
44
45
|
&& (parts.length === 1 || (anchor && parts.length === 2 && /^[A-Za-z0-9][A-Za-z0-9_.:-]*$/.test(parts[1])));
|
|
45
46
|
if (!valid) bad.push(`${label} must be a repository-relative path${anchor ? ' with an optional #anchor' : ''}`);
|
|
@@ -51,7 +52,7 @@ function unknownKeys(item, allowed, label, bad) {
|
|
|
51
52
|
|
|
52
53
|
function nonEmptyList(value, label, bad) {
|
|
53
54
|
if (!Array.isArray(value) || !value.length) { bad.push(`${label} must be a non-empty list`); return false; }
|
|
54
|
-
const normalized = value.map((value) => JSON.stringify(value, (_key, item) => isPlainObject(item) ? Object.fromEntries(Object.keys(item).sort().map((key) => [key, item[key]])) : item));
|
|
55
|
+
const normalized = value.map((value) => JSON.stringify(value, (_key, item) => isPlainObject(item) ? Object.fromEntries(Object.keys(item).sort(byCodeUnit).map((key) => [key, item[key]])) : item));
|
|
55
56
|
if (new Set(normalized).size !== value.length) bad.push(`${label} repeats a value`);
|
|
56
57
|
return true;
|
|
57
58
|
}
|
|
@@ -8,7 +8,7 @@ const DEPLOY = /(?:^|\/)deploy\//;
|
|
|
8
8
|
const SCRIPT = /(?:^|\/)scripts\//;
|
|
9
9
|
const COMPOSE = /(?:^|\/)(?:docker-)?compose(?:[.-][^/]*)?\.ya?ml$/;
|
|
10
10
|
const DOCKERFILE = /(?:^|\/)Dockerfile(?:\.[^/]*)?$/;
|
|
11
|
-
const CREDENTIAL_FILE = /(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|secrets\/)
|
|
11
|
+
const CREDENTIAL_FILE = /(?:(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|secrets\/))|(?:\.enc$)/i;
|
|
12
12
|
const HOST_CRON = /\b(?:crontab|anacron|crond|supercronic)\b|\/etc\/cron(?:\b|[./])|\bcron\.d\b|\/var\/spool\/cron\b|@(?:reboot|yearly|annually|monthly|weekly|daily|midnight|hourly)\b|\b[\w.@-]+\.timer\b|\bOn(?:Calendar|BootSec|UnitActiveSec|ActiveSec)\s*=|(?:^|[\s;|&"'])at\s+(?:(?:-[a-z]+\s+)*)((?:-[ft]\b)|now\b|tomorrow\b|\d{1,2}:\d{2}\b|midnight\b|noon\b)/im;
|
|
13
13
|
const HOST_CRON_PATH = /(?:^|\/)(?:crontab(?:$|\/)|cron\.d(?:\/|$)|[^/]+\.(?:timer|crontab)$)/;
|
|
14
14
|
const CRON_NUMERIC_FIELD = /^[\d*,/-]+$/;
|
|
@@ -228,7 +228,7 @@ function injectionTokens(repoRoot, tracked) {
|
|
|
228
228
|
const ts = loadTypescript(repoRoot, path.dirname(fileURLToPath(import.meta.url)));
|
|
229
229
|
if (!ts) return tokens;
|
|
230
230
|
for (const file of tracked) {
|
|
231
|
-
if (!/\.[cm]?[jt]sx?$/.test(file) || /(?:^|\/)(?:tests?|__tests__)
|
|
231
|
+
if (!/\.[cm]?[jt]sx?$/.test(file) || /(?:(?:^|\/)(?:tests?|__tests__)\/)|(?:(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$)/.test(file)) continue;
|
|
232
232
|
const text = readText(repoRoot, file) ?? '';
|
|
233
233
|
declaredTokens(ts, ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true), tokens);
|
|
234
234
|
}
|
|
@@ -37,7 +37,7 @@ function wireSchemaDigest(repoRoot, snapshots) {
|
|
|
37
37
|
for (const snapshot of [...snapshots].sort(byCodeUnit)) {
|
|
38
38
|
let source;
|
|
39
39
|
try { source = fs.readFileSync(path.join(repoRoot, snapshot), 'utf8'); } catch { return null; }
|
|
40
|
-
hash.update(`${snapshot}\0${source.
|
|
40
|
+
hash.update(`${snapshot}\0${source.replaceAll('\r\n', '\n')}\0`);
|
|
41
41
|
}
|
|
42
42
|
return hash.digest('hex');
|
|
43
43
|
}
|
|
@@ -79,7 +79,7 @@ function refProof(value) {
|
|
|
79
79
|
if (typeof value !== 'string') return 0;
|
|
80
80
|
const expression = unwrap(workflowExpression(value));
|
|
81
81
|
const alternatives = splitBoolean(expression, '||');
|
|
82
|
-
if (alternatives.length > 1) return alternatives.map(refProof).reduce((a, b) => a & b);
|
|
82
|
+
if (alternatives.length > 1) return alternatives.map(refProof).reduce((a, b) => a & b, RELEASE_REF);
|
|
83
83
|
const conjuncts = splitBoolean(expression, '&&');
|
|
84
84
|
if (conjuncts.length > 1) return conjuncts.map(refProof).reduce((a, b) => a | b, 0);
|
|
85
85
|
if (/^startsWith\(\s*github\.ref\s*,\s*(['"])refs\/tags\/v\1\s*\)$/.test(expression)) return RELEASE_REF;
|
|
@@ -7,7 +7,7 @@ const PLAINTEXT_LOAD = 'HFS_SCRIPT_PLAINTEXT_LOAD';
|
|
|
7
7
|
const XTRACE = 'HFS_SCRIPT_XTRACE';
|
|
8
8
|
const SERVICE_ROLE = 'HFS_SERVICE_ROLE_SCOPE';
|
|
9
9
|
const SUPABASE_OWNER = 'be.integrations.supabase';
|
|
10
|
-
const SECRET_FILE = /(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|\.nivo-lite(?:\/|$))
|
|
10
|
+
const SECRET_FILE = /(?:(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|\.nivo-lite(?:\/|$)))|(?:\.enc$)/iu;
|
|
11
11
|
const EXCLUDED_TREE = /(?:^|\/)(?:node_modules|\.git|\.claude|\.secrets|secrets|tests?|__tests__|docs|coverage|dist|build)(?:\/|$)/u;
|
|
12
12
|
const TEST_FILE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$/u;
|
|
13
13
|
const SOURCE = /\.(?:[cm]?[jt]sx?|sh|bash|zsh|ksh|ps1|psm1|bat|cmd|py|rb|pl)$/iu;
|
|
@@ -13,7 +13,7 @@ const SCRIPTS_DIR = /(?:^|\/)scripts\//;
|
|
|
13
13
|
const PACKAGE_JSON = /(?:^|\/)package\.json$/;
|
|
14
14
|
const SOURCE_FILE = /(?:^|\/)[^/.]+$|\.(?:sh|bash|zsh|mjs|cjs|js|ts|mts|py|ya?ml)$/;
|
|
15
15
|
const TEST_FILE = /\.(?:spec|test)\.[cm]?[jt]s$/;
|
|
16
|
-
const CREDENTIAL_FILE = /(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|secrets\/)
|
|
16
|
+
const CREDENTIAL_FILE = /(?:(?:^|\/)(?:secrets?\.env(?:\.[^/]*)?|\.env(?:\.[^/]*)?|secrets\/))|(?:\.enc$)/i;
|
|
17
17
|
const VERB_FILE = /^(render|provision|converge|verify|prune|rollback|release|backup|prepare|doctor|jobs|public)\.(?:mjs|js|cjs|sh)$/;
|
|
18
18
|
const COMMENT_LINE = /^[ \t]*(?:#|\/\/).*$/gm;
|
|
19
19
|
const LINE_CONTINUATION = /\\\r?\n\s*/g;
|
|
@@ -26,7 +26,7 @@ const DECLARATION = '.starcistacks/application-stacks.yaml';
|
|
|
26
26
|
const MAX_BYTES = 1024 * 1024;
|
|
27
27
|
const SOURCE = /^(?:be\/)?(?:src|apps)\/.*\.[cm]?[jt]s$/;
|
|
28
28
|
const TEST = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]s$|\/(?:tests|__tests__)\//;
|
|
29
|
-
const SECRET = /(?:^|\/)(?:secrets|\.claude|\.aws|\.codex)(?:\/|$)|(?:^|\/)\.env(?:\.|$)
|
|
29
|
+
const SECRET = /(?:(?:^|\/)(?:secrets|\.claude|\.aws|\.codex)(?:\/|$))|(?:(?:^|\/)\.env(?:\.|$))|(?:\.(?:enc|pem|key|p12|pfx)$)/i;
|
|
30
30
|
const CLI_ENTRY = /^(?:be\/)?dist\/apps\/cli\/[A-Za-z0-9_./-]+\.(?:js|mjs|cjs)$/;
|
|
31
31
|
const mapping = value => plain(value) ? value : {};
|
|
32
32
|
const present = value => typeof value === 'string' && Boolean(value.trim());
|
|
@@ -16,7 +16,7 @@ const mapping = value => plain(value) ? value : {};
|
|
|
16
16
|
const present = value => typeof value === 'string' && Boolean(value.trim());
|
|
17
17
|
const VERSION = /^[1-9][0-9]*$/;
|
|
18
18
|
const VERSIONED_SECRET = /-v[1-9][0-9]*$/;
|
|
19
|
-
const SENSITIVE_KEY = /password|passwd|secret|token|credential|private[_-]?key|api[_-]?key|service[_-]?role|(?:^|_)key
|
|
19
|
+
const SENSITIVE_KEY = /password|passwd|secret|token|credential|private[_-]?key|api[_-]?key|service[_-]?role|(?:(?:^|_)key$)/i;
|
|
20
20
|
const SOURCE = /^be\/(?:apps|src)\/.*\.[cm]?[jt]sx?$/;
|
|
21
21
|
const TEST_SOURCE = /(?:\.(?:spec|test)|-spec)\.[cm]?[jt]sx?$|\/tests\//;
|
|
22
22
|
const REALTIME_SOURCE = /^be\/src\/features\/realtime\/[^/]+\//;
|
|
@@ -32,6 +32,7 @@ function appendShellCommand(commands, state, next) {
|
|
|
32
32
|
}
|
|
33
33
|
|
|
34
34
|
function shellLineCommands(line, commands) {
|
|
35
|
+
/** @type {{quote: string | null, command: string, separator: string | null}} */
|
|
35
36
|
const state = { quote: null, command: '', separator: null };
|
|
36
37
|
for (let i = 0; i < line.length; i++) {
|
|
37
38
|
const char = line[i];
|
|
@@ -76,7 +76,7 @@ function expressions(value) {
|
|
|
76
76
|
|
|
77
77
|
/** Source markers travel inside parsed scalar expressions, preserving flow maps, quoted keys and repeated values without matching comments. */
|
|
78
78
|
export function workflowContextFindings({ path: file, text }) {
|
|
79
|
-
const marked = text.
|
|
79
|
+
const marked = text.replaceAll('${{', (match, offset) => `${match} __hfs_context_line_${text.slice(0, offset).split('\n').length}__ `);
|
|
80
80
|
const findings = [];
|
|
81
81
|
function inspect(value, keys) {
|
|
82
82
|
const allowed = allowedContexts(keys);
|
|
@@ -22,7 +22,7 @@ function checksumStep(checksum, value) {
|
|
|
22
22
|
function checksumWords(prefix, words) {
|
|
23
23
|
const codes = [...prefix].map((character) => character.codePointAt(0));
|
|
24
24
|
const expanded = [...codes.map((code) => code >>> 5), 0, ...codes.map((code) => code & 31)];
|
|
25
|
-
return [...expanded, ...words].reduce(checksumStep, 1);
|
|
25
|
+
return [...expanded, ...words].reduce((checksum, value) => checksumStep(checksum, value), 1);
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
function convertBits(input, from, to, pad) {
|
|
@@ -40,6 +40,8 @@ export const pathsOverlap = (a, b) => sameOrUnder(a, b) || sameOrUnder(b, a);
|
|
|
40
40
|
* `trim` blanks at the ends, `collapse` folds '//' runs to '/', `dot` drops a leading './',
|
|
41
41
|
* `glob` strips a trailing glob ('star': '*'/'**' with their slash, 'double': a '/**' only),
|
|
42
42
|
* `fold` lowercases. A trailing '/' always goes. Null reads as ''.
|
|
43
|
+
* @param {unknown} p
|
|
44
|
+
* @param {{trim?: boolean, collapse?: boolean, dot?: boolean, glob?: string|null, fold?: boolean}} [options]
|
|
43
45
|
*/
|
|
44
46
|
export const normPath = (p, { trim = false, collapse = false, dot = false, glob = null, fold = false } = {}) => {
|
|
45
47
|
let s = String(p ?? '');
|
|
@@ -173,7 +173,7 @@ function readmeBadgeText(text, manifestText) {
|
|
|
173
173
|
|
|
174
174
|
/** Whole-file secret judgement exempts only declared public badge values and preserves original line numbers and every provider shape. */
|
|
175
175
|
export function secretHitsInFile(file, fullText, { manifestText } = {}) {
|
|
176
|
-
const normalized = file.
|
|
176
|
+
const normalized = file.replaceAll('\\', '/');
|
|
177
177
|
let text = fullText;
|
|
178
178
|
if (/(?:^|\/)\.starcistacks\/application-stacks\.yaml$/.test(normalized)) text = manifestBadgeText(text);
|
|
179
179
|
if (/(?:^|\/)README\.md$/.test(normalized)) text = readmeBadgeText(text, manifestText);
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
/** The expression under the wrappers selected by an AST check's contract. */
|
|
2
2
|
export function unwrapExpression(ts, expression, { awaitExpressions = false, typeAssertions = true, nonNullExpressions = true, satisfiesExpressions = true } = {}) {
|
|
3
|
-
while (expression
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
while (expression) {
|
|
4
|
+
const wrapper = ts.isParenthesizedExpression(expression) || ts.isAsExpression(expression)
|
|
5
|
+
|| (typeAssertions && ts.isTypeAssertionExpression(expression)) || (nonNullExpressions && ts.isNonNullExpression(expression))
|
|
6
|
+
|| (satisfiesExpressions && (ts.isSatisfiesExpression?.(expression) ?? false))
|
|
7
|
+
|| (awaitExpressions && ts.isAwaitExpression(expression));
|
|
8
|
+
if (!wrapper) break;
|
|
9
|
+
expression = expression.expression;
|
|
10
|
+
}
|
|
7
11
|
return expression;
|
|
8
12
|
}
|
|
9
13
|
|