declapract-typescript-ehmpathy 0.47.64 → 0.47.65

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.
@@ -37,6 +37,7 @@ jobs:
37
37
  with:
38
38
  creds-aws-region: us-east-1
39
39
  creds-aws-role-arn: arn:aws:iam::@declapract{variable.awsAccountId.dev}:role/@declapract{variable.projectName}-github-actions-dev
40
+ secrets: inherit # keyrack firewall in .test.yml filters to declared keys
40
41
 
41
42
  dev:
42
43
  uses: ./.github/workflows/.deploy-expo.yml
@@ -206,6 +206,16 @@ jobs:
206
206
  - name: prepare:rhachet
207
207
  run: npm run prepare:rhachet --if-present
208
208
 
209
+ # .why = keyrack firewall translates and validates secrets before tests run
210
+ # - filters to declared keys in keyrack.yml
211
+ # - translates mechanisms (e.g., GitHub App → ghs_* token)
212
+ # - blocks dangerous patterns (ghp_*, AKIA*, etc.)
213
+ # - exports to $GITHUB_ENV with mask applied
214
+ - name: keyrack firewall
215
+ run: npx rhachet keyrack firewall --env test --from 'json(env://SECRETS_JSON)' --into github.actions
216
+ env:
217
+ SECRETS_JSON: ${{ toJSON(secrets) }}
218
+
209
219
  - name: get aws auth, if creds supplied
210
220
  if: ${{ inputs.creds-aws-role-arn }}
211
221
  uses: aws-actions/configure-aws-credentials@v4
@@ -280,6 +290,16 @@ jobs:
280
290
  - name: prepare:rhachet
281
291
  run: npm run prepare:rhachet --if-present
282
292
 
293
+ # .why = keyrack firewall translates and validates secrets before tests run
294
+ # - filters to declared keys in keyrack.yml
295
+ # - translates mechanisms (e.g., GitHub App → ghs_* token)
296
+ # - blocks dangerous patterns (ghp_*, AKIA*, etc.)
297
+ # - exports to $GITHUB_ENV with mask applied
298
+ - name: keyrack firewall
299
+ run: npx rhachet keyrack firewall --env test --from 'json(env://SECRETS_JSON)' --into github.actions
300
+ env:
301
+ SECRETS_JSON: ${{ toJSON(secrets) }}
302
+
283
303
  - name: get aws auth, if creds supplied
284
304
  if: ${{ inputs.creds-aws-role-arn }}
285
305
  uses: aws-actions/configure-aws-credentials@v4
@@ -1,114 +1,15 @@
1
- import { existsSync } from 'node:fs';
2
- import { join } from 'node:path';
3
-
4
- import type { FileCheckFunction, FileFixFunction } from 'declapract';
5
- import { keyrack, type KeyrackGrantAttempt } from 'rhachet/keyrack';
1
+ import { FileCheckType, type FileFixFunction } from 'declapract';
6
2
 
7
3
  /**
8
- * .what = gets keyrack key names for test env from target project
9
- * .why = single source of truth for key discovery
4
+ * .what = validates .test.yml matches the template exactly
5
+ * .why = keyrack firewall handles secrets at runtime; no build-time injection needed
10
6
  */
11
- const getKeyrackKeys = async (projectRootDir: string): Promise<string[]> => {
12
- const keyrackYmlPath = join(projectRootDir, '.agent/keyrack.yml');
13
- if (!existsSync(keyrackYmlPath)) return [];
14
-
15
- const keys = (await keyrack.get({
16
- for: { repo: true },
17
- env: 'test',
18
- })) as KeyrackGrantAttempt[];
19
- if (!keys.length) return [];
20
-
21
- // extract key names from slugs (format: org.env.KEY_NAME)
22
- // .note = KeyrackGrantAttempt is a union of 4 variants (granted/absent/locked/blocked)
23
- // per rhachet/dist/domain.objects/keyrack/KeyrackGrantAttempt.d.ts:8
24
- // granted has slug at grant.slug; others have slug at top level
25
- return keys.map((k) =>
26
- (k.status === 'granted' ? k.grant.slug : k.slug).split('.').pop()!,
27
- );
28
- };
7
+ export const check = FileCheckType.EQUALS;
29
8
 
30
9
  /**
31
- * .what = builds the expected .test.yml content with keyrack secrets injected
32
- * .why = single source of truth for both check and fix
33
- */
34
- export const buildExpectedContent = (input: {
35
- template: string;
36
- keys: string[];
37
- }): string => {
38
- let result = input.template;
39
-
40
- // if no keys, return template as-is
41
- if (!input.keys.length) {
42
- return result;
43
- }
44
-
45
- // build secrets declaration block for workflow_call
46
- const secretsDeclaration = input.keys
47
- .map(
48
- (key) =>
49
- ` ${key}:\n description: "api key for ${key.toLowerCase().replace(/_/g, ' ')}"\n required: false`,
50
- )
51
- .join('\n');
52
-
53
- // build env block for test-integration job
54
- const envBlock = input.keys
55
- .map((key) => ` ${key}: \${{ secrets.${key} }}`)
56
- .join('\n');
57
-
58
- // insert secrets declaration after workflow_call inputs
59
- // look for the pattern: inputs: ... (multiline) followed by blank line or permissions:
60
- result = result.replace(
61
- /(on:\n workflow_call:\n inputs:\n(?: [^\n]+\n)+)/,
62
- `$1 secrets:\n${secretsDeclaration}\n\n`,
63
- );
64
-
65
- // insert env block to test-integration job
66
- // add env block after needs: [install] line
67
- result = result.replace(
68
- /(test-integration:\n runs-on: ubuntu-24\.04\n needs: \[install\]\n)/,
69
- `$1 env:\n${envBlock}\n`,
70
- );
71
-
72
- return result;
73
- };
74
-
75
- /**
76
- * .what = ensures .test.yml matches expected content with keyrack secrets
77
- * .why = enables integration tests to access required api keys via github secrets
78
- */
79
- export const check: FileCheckFunction = async (contents, context) => {
80
- // get keyrack keys from project
81
- const keys = await getKeyrackKeys(context.getProjectRootDirectory());
82
-
83
- // build expected content from template + keys
84
- const expected = buildExpectedContent({
85
- template: context.declaredFileContents ?? '',
86
- keys,
87
- });
88
-
89
- // if contents don't match expected, best practice is violated
90
- if (contents !== expected) {
91
- throw new Error(
92
- 'file does not match expected content with keyrack secrets',
93
- );
94
- }
95
-
96
- // return = file matches expected (best practice followed)
97
- };
98
-
99
- /**
100
- * .what = fixes .test.yml to include keyrack secrets declaration and env vars
101
- * .why = ensures integration tests have access to required api keys
10
+ * .what = replaces .test.yml with the template content
11
+ * .why = ensures workflow has firewall step for keyrack secrets
102
12
  */
103
13
  export const fix: FileFixFunction = async (_contents, context) => {
104
- // get keyrack keys from project
105
- const keys = await getKeyrackKeys(context.getProjectRootDirectory());
106
-
107
- // build expected content from template + keys
108
- const expected = buildExpectedContent({
109
- template: context.declaredFileContents ?? '',
110
- keys,
111
- });
112
-
113
- return { contents: expected };
14
+ return { contents: context.declaredFileContents ?? '' };
114
15
  };
@@ -22,3 +22,4 @@ jobs:
22
22
  with:
23
23
  creds-aws-region: us-east-1
24
24
  creds-aws-role-arn: ${{ vars.CREDS_CICD_AWS_DEV_OIDC_ROLE_ARN }} # use aws auth via oidc, if this repo supplies it
25
+ secrets: inherit # keyrack firewall in .test.yml filters to declared keys
@@ -1,45 +1,15 @@
1
- import type { FileCheckFunction, FileFixFunction } from 'declapract';
2
- import { UnexpectedCodePathError } from 'helpful-errors';
3
-
4
- import { buildWorkflowSecretsBlock } from '../../../../../utils/buildWorkflowSecretsBlock';
1
+ import { FileCheckType, type FileFixFunction } from 'declapract';
5
2
 
6
3
  /**
7
- * .what = ensures test.yml matches expected content with apikey secrets block
8
- * .why = enables the reusable workflow to access github secrets
4
+ * .what = validates test.yml matches the template exactly
5
+ * .why = secrets: inherit passes all secrets to .test.yml; firewall handles key filter at runtime
9
6
  */
10
- export const check: FileCheckFunction = async (contents, context) => {
11
- // fail fast if template not found
12
- if (!context.declaredFileContents)
13
- throw new UnexpectedCodePathError('declaredFileContents not found', {
14
- relativeFilePath: context.relativeFilePath,
15
- });
16
-
17
- const expected = await buildWorkflowSecretsBlock(
18
- { template: context.declaredFileContents },
19
- context,
20
- );
21
-
22
- // if contents don't match expected, best practice is violated
23
- if (contents !== expected)
24
- throw new Error('file does not match expected content with apikey secrets');
25
-
26
- // return = file matches expected (best practice followed)
27
- };
7
+ export const check = FileCheckType.EQUALS;
28
8
 
29
9
  /**
30
- * .what = fixes test.yml to include apikey secrets block
31
- * .why = ensures the reusable workflow receives required api keys
10
+ * .what = replaces test.yml with the template content
11
+ * .why = ensures caller workflow has secrets: inherit for firewall
32
12
  */
33
13
  export const fix: FileFixFunction = async (_contents, context) => {
34
- // fail fast if template not found
35
- if (!context.declaredFileContents)
36
- throw new UnexpectedCodePathError('declaredFileContents not found', {
37
- relativeFilePath: context.relativeFilePath,
38
- });
39
-
40
- const expected = await buildWorkflowSecretsBlock(
41
- { template: context.declaredFileContents },
42
- context,
43
- );
44
- return { contents: expected };
14
+ return { contents: context.declaredFileContents ?? '' };
45
15
  };
@@ -19,6 +19,7 @@ jobs:
19
19
  with:
20
20
  creds-aws-region: us-east-1
21
21
  creds-aws-role-arn: ${{ vars.CREDS_CICD_AWS_DEV_OIDC_ROLE_ARN }} # use aws auth via oidc, if this repo supplies it
22
+ secrets: inherit # keyrack firewall in .test.yml filters to declared keys
22
23
 
23
24
  publish:
24
25
  uses: ./.github/workflows/.publish-npm.yml
@@ -1,45 +1,15 @@
1
- import type { FileCheckFunction, FileFixFunction } from 'declapract';
2
- import { UnexpectedCodePathError } from 'helpful-errors';
3
-
4
- import { buildWorkflowSecretsBlock } from '../../../../../utils/buildWorkflowSecretsBlock';
1
+ import { FileCheckType, type FileFixFunction } from 'declapract';
5
2
 
6
3
  /**
7
- * .what = ensures publish.yml matches expected content with apikey secrets block
8
- * .why = enables the reusable workflow to access github secrets during publish
4
+ * .what = validates publish.yml matches the template exactly
5
+ * .why = secrets: inherit passes all secrets to .test.yml; firewall handles key filter at runtime
9
6
  */
10
- export const check: FileCheckFunction = async (contents, context) => {
11
- // fail fast if template not found
12
- if (!context.declaredFileContents)
13
- throw new UnexpectedCodePathError('declaredFileContents not found', {
14
- relativeFilePath: context.relativeFilePath,
15
- });
16
-
17
- const expected = await buildWorkflowSecretsBlock(
18
- { template: context.declaredFileContents },
19
- context,
20
- );
21
-
22
- // if contents don't match expected, best practice is violated
23
- if (contents !== expected)
24
- throw new Error('file does not match expected content with apikey secrets');
25
-
26
- // return = file matches expected (best practice followed)
27
- };
7
+ export const check = FileCheckType.EQUALS;
28
8
 
29
9
  /**
30
- * .what = fixes publish.yml to include apikey secrets block
31
- * .why = ensures the reusable workflow receives required api keys during publish
10
+ * .what = replaces publish.yml with the template content
11
+ * .why = ensures caller workflow has secrets: inherit for firewall
32
12
  */
33
13
  export const fix: FileFixFunction = async (_contents, context) => {
34
- // fail fast if template not found
35
- if (!context.declaredFileContents)
36
- throw new UnexpectedCodePathError('declaredFileContents not found', {
37
- relativeFilePath: context.relativeFilePath,
38
- });
39
-
40
- const expected = await buildWorkflowSecretsBlock(
41
- { template: context.declaredFileContents },
42
- context,
43
- );
44
- return { contents: expected };
14
+ return { contents: context.declaredFileContents ?? '' };
45
15
  };
@@ -41,6 +41,7 @@ jobs:
41
41
  with:
42
42
  creds-aws-region: us-east-1
43
43
  creds-aws-role-arn: ${{ vars.CREDS_CICD_AWS_DEV_OIDC_ROLE_ARN }} # use aws auth via oidc, if this repo supplies it
44
+ secrets: inherit # keyrack firewall in .test.yml filters to declared keys
44
45
 
45
46
  dev:
46
47
  uses: ./.github/workflows/.deploy-sls.yml
@@ -1,45 +1,15 @@
1
- import type { FileCheckFunction, FileFixFunction } from 'declapract';
2
- import { UnexpectedCodePathError } from 'helpful-errors';
3
-
4
- import { buildWorkflowSecretsBlock } from '../../../../../utils/buildWorkflowSecretsBlock';
1
+ import { FileCheckType, type FileFixFunction } from 'declapract';
5
2
 
6
3
  /**
7
- * .what = ensures deploy.yml matches expected content with apikey secrets block
8
- * .why = enables the reusable workflow to access github secrets during deploy
4
+ * .what = validates deploy.yml matches the template exactly
5
+ * .why = secrets: inherit passes all secrets to .test.yml; firewall handles key filter at runtime
9
6
  */
10
- export const check: FileCheckFunction = async (contents, context) => {
11
- // fail fast if template not found
12
- if (!context.declaredFileContents)
13
- throw new UnexpectedCodePathError('declaredFileContents not found', {
14
- relativeFilePath: context.relativeFilePath,
15
- });
16
-
17
- const expected = await buildWorkflowSecretsBlock(
18
- { template: context.declaredFileContents },
19
- context,
20
- );
21
-
22
- // if contents don't match expected, best practice is violated
23
- if (contents !== expected)
24
- throw new Error('file does not match expected content with apikey secrets');
25
-
26
- // return = file matches expected (best practice followed)
27
- };
7
+ export const check = FileCheckType.EQUALS;
28
8
 
29
9
  /**
30
- * .what = fixes deploy.yml to include apikey secrets block
31
- * .why = enables the reusable workflow receives required api keys during deploy
10
+ * .what = replaces deploy.yml with the template content
11
+ * .why = ensures caller workflow has secrets: inherit for firewall
32
12
  */
33
13
  export const fix: FileFixFunction = async (_contents, context) => {
34
- // fail fast if template not found
35
- if (!context.declaredFileContents)
36
- throw new UnexpectedCodePathError('declaredFileContents not found', {
37
- relativeFilePath: context.relativeFilePath,
38
- });
39
-
40
- const expected = await buildWorkflowSecretsBlock(
41
- { template: context.declaredFileContents },
42
- context,
43
- );
44
- return { contents: expected };
14
+ return { contents: context.declaredFileContents ?? '' };
45
15
  };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "declapract-typescript-ehmpathy",
3
3
  "author": "ehmpathy",
4
4
  "description": "declapract best practices declarations for typescript",
5
- "version": "0.47.64",
5
+ "version": "0.47.65",
6
6
  "license": "MIT",
7
7
  "main": "src/index.js",
8
8
  "repository": "ehmpathy/declapract-typescript-ehmpathy",
@@ -75,12 +75,12 @@
75
75
  "esbuild-register": "3.6.0",
76
76
  "husky": "8.0.3",
77
77
  "jest": "30.2.0",
78
- "rhachet": "1.39.9",
79
- "rhachet-brains-anthropic": "0.4.0",
78
+ "rhachet": "1.41.1",
79
+ "rhachet-brains-anthropic": "0.4.1",
80
80
  "rhachet-brains-xai": "0.3.3",
81
- "rhachet-roles-bhrain": "0.23.10",
82
- "rhachet-roles-bhuild": "0.17.0",
83
- "rhachet-roles-ehmpathy": "1.34.23",
81
+ "rhachet-roles-bhrain": "0.27.5",
82
+ "rhachet-roles-bhuild": "0.21.1",
83
+ "rhachet-roles-ehmpathy": "1.35.0",
84
84
  "tsc-alias": "1.8.10",
85
85
  "tsx": "4.20.6",
86
86
  "type-fns": "1.21.2",
@@ -1,50 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'os';
3
- import path from 'node:path';
4
-
5
- import type { KeyrackGrantAttempt } from 'rhachet/keyrack';
6
-
7
- /**
8
- * .what = creates a mock context with keyrack config in a temp directory
9
- * .why = enables test of buildWorkflowSecretsBlock with different keyrack configs
10
- */
11
- export const withKeyrackContext = async (
12
- input: { keys: string[] },
13
- fn: (context: { getProjectRootDirectory: () => string }) => Promise<void>,
14
- ): Promise<void> => {
15
- // create temp dir with keyrack.yml
16
- const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
17
- const agentDir = path.join(tempDir, '.agent');
18
- fs.mkdirSync(agentDir, { recursive: true });
19
- fs.writeFileSync(
20
- path.join(agentDir, 'keyrack.yml'),
21
- `org: test\nenv.test:\n${input.keys.map((k) => ` - ${k}`).join('\n')}`,
22
- );
23
-
24
- // mock keyrack.get to return grant attempts
25
- const mockGrants: KeyrackGrantAttempt[] = input.keys.map((key) => ({
26
- status: 'granted' as const,
27
- grant: {
28
- slug: `test.test.${key}`,
29
- value: 'mock-value',
30
- mech: 'PERMANENT_VIA_REPLICA' as const,
31
- vault: 'os.direct' as const,
32
- },
33
- }));
34
-
35
- // store original module
36
- const keyrackModule = await import('rhachet/keyrack');
37
- const originalGet = keyrackModule.keyrack.get;
38
-
39
- // replace with mock
40
- (keyrackModule.keyrack as { get: typeof originalGet }).get = async () =>
41
- mockGrants;
42
-
43
- try {
44
- await fn({ getProjectRootDirectory: () => tempDir });
45
- } finally {
46
- // restore original
47
- (keyrackModule.keyrack as { get: typeof originalGet }).get = originalGet;
48
- fs.rmSync(tempDir, { recursive: true, force: true });
49
- }
50
- };
@@ -1,46 +0,0 @@
1
- import { existsSync } from 'node:fs';
2
- import { join } from 'node:path';
3
-
4
- import { keyrack, type KeyrackGrantAttempt } from 'rhachet/keyrack';
5
-
6
- /**
7
- * .what = builds workflow content with keyrack secrets block for .test.yml
8
- * .why = single source of truth for test.yml, publish.yml, deploy.yml check+fix
9
- */
10
- export const buildWorkflowSecretsBlock = async (
11
- input: { template: string },
12
- context: { getProjectRootDirectory: () => string },
13
- ): Promise<string> => {
14
- // check if keyrack.yml exists
15
- const keyrackYmlPath = join(
16
- context.getProjectRootDirectory(),
17
- '.agent/keyrack.yml',
18
- );
19
- if (!existsSync(keyrackYmlPath)) return input.template;
20
-
21
- // get required keys from keyrack sdk
22
- const keys = (await keyrack.get({
23
- for: { repo: true },
24
- env: 'test',
25
- })) as KeyrackGrantAttempt[];
26
- if (!keys.length) return input.template;
27
-
28
- // extract key names from slugs (format: org.env.KEY_NAME)
29
- // .note = KeyrackGrantAttempt is a union of 4 variants (granted/absent/locked/blocked)
30
- // per rhachet/dist/domain.objects/keyrack/KeyrackGrantAttempt.d.ts:8
31
- // granted has slug at grant.slug; others have slug at top level
32
- const keyrackVars = keys.map((k) =>
33
- (k.status === 'granted' ? k.grant.slug : k.slug).split('.').pop()!,
34
- );
35
-
36
- // build secrets block
37
- const secretsBlock = keyrackVars
38
- .map((key) => ` ${key}: \${{ secrets.${key} }}`)
39
- .join('\n');
40
-
41
- // find jobs that call .test.yml with 'with:' block and add secrets block after
42
- return input.template.replace(
43
- /(uses: \.\/\.github\/workflows\/\.test\.yml\n(?: if: [^\n]+\n)? with:\n(?: [^\n]+\n)+)/g,
44
- `$1 secrets:\n${secretsBlock}\n`,
45
- );
46
- };