Dashboard › craft › Session 0jlJMuwk4Zmg
0jlJMuwk4ZmgeQIP6Return your completed security review result now. If incomplete, finish it read-only and report cited findings and MERGE/DO-NOT-MERGE. Do not edit.
<path>/home/byk/Code/getsentry/craft-workspaces-schema/src/commands/publish.ts</path>
<type>file</type>
<content>
550: sysCommand = '/bin/bash';
551: args = [DEFAULT_POST_RELEASE_SCRIPT_PATH];
552: } else {
553: // Not running post-release command
554: logger.info(
555: `Not running the optional post-release command: '${DEFAULT_POST_RELEASE_SCRIPT_PATH}' not found`,
556: );
557: return false;
558: }
559: args = [...args, '', newVersion];
560: logger.info(`Running the post-release command...`);
561: await spawnProcess(sysCommand as string, args as string[], {
562: env: buildReleaseCommandEnv({ CRAFT_RELEASED_VERSION: newVersion }),
563: });
564: return true;
565: }
566:
567: /**
568: * Body of 'publish' command
569: *
570: * @param argv Command-line arguments
571: */
572: export async function publishMain(argv: PublishOptions): Promise<any> {
573: // Get publishing configuration
574: const config = getConfiguration() || {};
575: ...
<path>/home/byk/Code/getsentry/craft-workspaces-schema/src/utils/publishState.ts</path> <type>file</type> <content> 1: /** 2: * Helpers for locating Craft's publish-state file in a path that is NOT 3: * writable by the repository being published. 4: * 5: * Background: `craft publish` writes a small JSON file listing targets 6: * that have completed so a resumed run can skip them. Before this module 7: * existed, the file lived at `.craft-publish-<version>.json` in the 8: * project's cwd. That path is inside the repository checkout, so any 9: * committed file at the same path (or any earlier CI step) could 10: * pre-populate the "published" set and trick Craft into silently 11: * skipping targets. 12: * 13: * The file now lives under `$XDG_STATE_HOME/craft/` (falling back to 14: * `$HOME/.local/state/craft/`). The filename is keyed on 15: * owner, repo, a hash of cwd (to disambiguate monorepo subpaths), and 16: * the version being published. ...
<path>/home/byk/Code/getsentry/craft-workspaces-schema/src/targets/base.ts</path>
<type>file</type>
<content>
1: import { logger as loggerRaw } from '../logger';
2: import { GitHubGlobalConfig, TargetConfig } from '../schemas/project_config';
3: import {
4: parseFilterOptions,
5: RawFilterOptions,
6: ParsedFilterOptions,
7: } from '../artifact_providers/base';
8: import { stringToRegexp } from '../utils/filters';
9: import {
10: BaseArtifactProvider,
11: RemoteArtifact,
12: } from '../artifact_providers/base';
13:
14: /**
15: * Base class for all remote targets
16: */
17: export class BaseTarget {
18: public readonly id: string;
19: protected readonly logger: typeof loggerRaw;
20: /** Artifact provider */
21: public readonly artifactProvider: BaseArtifactProvider;
22: /** Unparsed target configuration */
23: public readonly config: TargetConfig;
24: /** Artifact filtering options for the target */
25: public readonly filterOptions: ParsedFilterOptions;
26: ...
None.
Prototype-named workspace selectors bypass unknown-workspace validation and release the base configuration.
resolveWorkspaceConfig() reads workspaces[workspaceName] without an own-property check (src/config.ts:114-116). Names inherited from Object.prototype such as toString, constructor, or __proto__ are truthy, so validation passes; Object.entries() yields no workspace overrides and the function returns the top-level release config (src/config.ts:129-160). applyWorkspaceSelection() then accepts that result (src/config.ts:231-239). A malformed or attacker-controlled CRAFT_WORKSPACE can therefore route a workspace release to the implicit base release instead of failing closed.
Publish-resume state is shared across independently versioned workspaces.
The state-file key includes only owner, repo, cwd, and version (src/utils/publishState.ts:78-90), while the new workspace identity is omitted when publish resolves its state path (src/commands/publish.ts:685-699). A successful cli publish of version X can therefore cause an mcp publish of the same version in the same repo/cwd to load its state and remove matching target IDs (src/commands/publish.ts:719-739). Default target IDs are only the target name (src/targets/base.ts:29-33), so two workspace github targets both collide as github. The second release silently skips publication.
None.
FAIL — DO-NOT-MERGE.
Re-review the two previous MAJOR security findings against the current worktree only: (1) prototype-named workspace selectors, and (2) publish-state collisions across workspaces. Read-only; do not edit. Verify tests fail conceptually on old implementation and pass on new, inspect exact diffs and relevant behavior. Report PASS/FAIL with file:line citations and MERGE/DO-NOT-MERGE.
No files found
ab63572 fix(publish): isolate workspace release state 086a633 fix(config): address workspace review feedback c099bbf Merge branch 'master' into feat/workspaces-schema e5a7f97 fix(deps): bump vulnerable packages and add overrides (#866) 8cc5425 feat(targets): add vercel deploy target (#865)
feat/workspaces-schema 8cc54254f2e9489abbfdc028df15e7effbbc6bb8 docs/package.json | 4 +- docs/pnpm-lock.yaml | 24 +- package.json | 14 +- pnpm-lock.yaml | 382 +++++-------------------------- src/__tests__/config.test.ts | 219 ++++++++++++++++++ src/commands/publish.ts | 24 +- src/config.ts | 247 +++++++++++++++++--- src/index.ts | 23 +- src/schemas/project_config.ts | 54 ++++- src/utils/__tests__/helpers.test.ts | 98 ++++++++ src/utils/__tests__/publishState.test.ts | 40 ++++ src/utils/helpers.ts | 51 +++++ src/utils/publishState.ts | 17 +- 13 files changed, 811 insertions(+), 386 deletions(-) diff --git a/src/__tests__/config.test.ts b/src/__tests__/config.test.ts index 243835d..90e145b 100644 --- ...
<path>/home/byk/Code/getsentry/craft-workspaces-schema/src/config.ts</path>
<type>file</type>
<content>
97: /**
98: * Merges a workspace's overrides onto the top-level (base) config, producing a
99: * flat `CraftProjectConfig` that the rest of craft consumes unchanged.
100: *
101: * Resolution rules:
102: * - Every release-relevant field defined on the workspace replaces the
103: * top-level value (shallow override; a workspace either declares a field or
104: * inherits it wholesale — we do not deep-merge arrays/objects, to keep
105: * behavior predictable).
106: * - `github` is shallow-merged (owner/repo/projectPath) so a workspace can
107: * override just `projectPath` while inheriting owner/repo.
108: * - `minVersion` and `workspaces` themselves are stripped from the result.
109: */
110: function resolveWorkspaceConfig(
111: base: CraftProjectConfig,
112: workspaceName: string,
113: ): CraftProjectConfig {
114: const workspaces = base.workspaces || ...
<path>/home/byk/Code/getsentry/craft-workspaces-schema/src/utils/publishState.ts</path>
<type>file</type>
<content>
58: /**
59: * Short (12-char) hex digest of the absolute cwd path. Used to
60: * disambiguate monorepo subpaths so `packages/foo` and `packages/bar`
61: * get separate state files even at the same version.
62: */
63: function shortCwdHash(cwd: string): string {
64: return createHash('sha1').update(cwd).digest('hex').slice(0, 12);
65: }
66:
67: /**
68: * Builds the filename for the publish-state file.
69: *
70: * With a resolvable GitHub config:
71: * `publish-state-<owner>-<repo>-<sha1(cwd)[:12]>-<version>.json`
72: *
73: * Without GitHub config (offline / non-GitHub test harnesses) the
74: * filename falls back to a cwd-hash-only form so Craft still refuses
75: * to write into the repo itself:
76: * `publish-state-<sha256(cwd)[:16]>-<version>.json`
77: *
78: * When a release workspace is selected, its losslessly encoded name is
79: * included ...
<path>/home/byk/Code/getsentry/craft-workspaces-schema/src/commands/publish.ts</path>
<type>file</type>
<content>
1: import { Arguments, Argv, CommandBuilder } from 'yargs';
2: import chalk from 'chalk';
3: import { existsSync, mkdirSync, readFileSync } from 'fs';
4:
5: import { safeFs } from '../utils/dryRun';
6: import { dirname, join } from 'path';
7: import * as shellQuote from 'shell-quote';
8: import stringLength from 'string-length';
9:
10: import {
11: getConfiguration,
12: getStatusProviderFromConfig,
13: getArtifactProviderFromConfig,
14: DEFAULT_RELEASE_BRANCH_NAME,
15: getGlobalGitHubConfig,
16: expandWorkspaceTargets,
17: getNoMergeConfig,
18: getActiveWorkspace,
19: } from '../config';
20: import { formatTable, logger } from '../logger';
21: import { TargetConfig } from '../schemas/project_config';
22: import { getAllTargetNames, getTargetByName, SpecialTarget } from '../targets';
23: import { BaseTarget } from '../targets/base';
24: import {
25: ...
<path>/home/byk/Code/getsentry/craft-workspaces-schema/src/commands/publish.ts</path>
<type>file</type>
<content>
674: for (const specialTarget of [SpecialTarget.All, SpecialTarget.None]) {
675: if (targetsToPublish.size > 1 && targetsToPublish.has(specialTarget)) {
676: logger.error(
677: `Target "${specialTarget}" specified together with other targets. Exiting.`,
678: );
679: return undefined;
680: }
681: }
682:
683: // Expand any npm workspace targets into individual package targets
684: let targetConfigList = await expandWorkspaceTargets(config.targets || []);
685:
686: // Resolve the GitHub config up front so we can key the publish-state
687: // file by owner/repo. `getGlobalGitHubConfig()` returns cached data on
688: // subsequent calls, so this is effectively free. ...
<path>/home/byk/Code/getsentry/craft-workspaces-schema/src/__tests__/config.test.ts</path>
<type>file</type>
<content>
280: // `workspaces` is stripped from the resolved config.
281: expect(config.workspaces).toBeUndefined();
282: });
283:
284: test('a different workspace resolves independently', () => {
285: setActiveWorkspace('mcp');
286: const config = loadConfigurationFromString(WS_CONFIG);
287: expect(config.releaseBranchPrefix).toBe('release/mcp');
288: expect(getGitTagPrefix()).toBe('mcp@');
289: expect(getVersioningPolicy()).toBe('calver');
290: // mcp did not override github.projectPath, so it inherits base github only.
291: expect(config.github).toEqual({ owner: 'getsentry', repo: 'toolkit' });
292: });
293:
294: test('errors when workspaces are defined but none is selected', () => {
295: setActiveWorkspace(undefined);
296: expect(() => loadConfigurationFromString(WS_CONFIG)).toThrow(
297: /defines workspaces; ...
<path>/home/byk/Code/getsentry/craft-workspaces-schema/src/utils/__tests__/publishState.test.ts</path>
<type>file</type>
<content>
1: import { describe, test, expect, beforeEach, afterEach } from 'vitest';
2: import { homedir } from 'os';
3: import { join } from 'path';
4:
5: import {
6: getCraftStateDir,
7: getPublishStateFilename,
8: getPublishStatePath,
9: } from '../publishState';
10:
11: describe('publishState', () => {
12: const savedEnv = { ...process.env };
13:
14: beforeEach(() => {
15: delete process.env.XDG_STATE_HOME;
16: });
17:
18: afterEach(() => {
19: process.env = { ...savedEnv };
20: });
21:
22: describe('getCraftStateDir', () => {
23: test('defaults to $HOME/.local/state/craft when XDG_STATE_HOME is unset', () => {
24: expect(getCraftStateDir()).toBe(
25: join(homedir(), '.local', 'state', 'craft'),
26: );
27: });
28:
29: test('honours XDG_STATE_HOME when set', () => {
30: ...
RUN v4.1.8 /home/byk/Code/getsentry/craft-workspaces-schema
Test Files 2 passed (2)
Tests 52 passed (52)
Start at 09:57:11
Duration 1.32s (transform 441ms, setup 0ms, import 1.22s, tests 56ms, environment 0ms)
Checking formatting...
All matched files use Prettier code style!
> @sentry/craft@2.29.0-dev.0 lint /home/byk/Code/getsentry/craft-workspaces-schema
> eslint --cache --cache-strategy content
/home/byk/Code/getsentry/craft-workspaces-schema/src/commands/publish.ts
453:14 warning '_abortError' is defined but never used @typescript-eslint/no-unused-vars
470:14 warning '_abortError' is defined but never used @typescript-eslint/no-unused-vars
486:16 warning '_statusError' is defined but never used @typescript-eslint/no-unused-vars
492:18 warning '_diffError' is defined but never used @typescript-eslint/no-unused-vars
498:16 warning '_abortError' is defined but never used ...
resolveWorkspaceConfig() now requires an own property before reading it: src/config.ts:114-124.constructor, toString, and __proto__, rather than merging an inherited object/function into the base release config.Unknown workspace: src/__tests__/config.test.ts:308-316.Fail-first proof: against the prior const workspace = workspaces[workspaceName]; if (!workspace) implementation, these inherited values are truthy; the test would not throw and would fail. It passes with Object.hasOwn().
src/utils/publishState.ts:82-99.publishMain() passes the active workspace into the state-path computation: src/commands/publish.ts:697-702.src/commands/publish.ts:722-742.cli and mcp, plus case/punctuation-distinct names: src/utils/__tests__/publishState.test.ts:70-99. They also preserve the legacy filename for non-workspace releases: src/utils/__tests__/publishState.test.ts:101-108.Fail-first proof: on the prior implementation, the workspace argument is ignored at runtime and both calls resolve to the identical owner/repo/cwd/version filename; expect(cli).not.toBe(mcp) at src/utils/__tests__/publishState.test.ts:83 fails. It passes with the current implementation.
tsc --noEmit: passed.MERGE.
Return the focused security recheck result now with PASS/FAIL and MERGE/DO-NOT-MERGE. Do not edit.
Prototype-named workspace selectors — PASS
Object.hasOwn(workspaces, workspaceName) rejects inherited names before resolution (src/config.ts:114-124). Regression coverage verifies constructor, toString, and __proto__ fail closed (src/__tests__/config.test.ts:308-316).
Publish-state collisions across workspaces — PASS
Workspace names are Base64URL-encoded into state filenames (src/utils/publishState.ts:82-99) and passed from publish execution (src/commands/publish.ts:697-702). Tests prove separate state files for same repo/cwd/version workspaces and collision-resistant names (src/utils/__tests__/publishState.test.ts:70-99).
Targeted tests: 52 passed. Typecheck and Prettier passed. Lint has no errors.
MERGE.
Date: Aug 25, 2026 * 🔴 [requested-security-review] (10:00) User requested the focused security recheck result immediately, requiring PASS/FAIL and MERGE/DO-NOT-MERGE, and instructed not to edit. * 🟡 (10:00) Assistant reported focused security recheck: prototype-named workspace selectors **PASS**; `Object.hasOwn(workspaces, workspaceName)` at `src/config.ts:114-124` rejects inherited selectors b…
Date: Aug 25, 2026 * 🔴 [requested-security-review] (09:45) User requested a read-only re-review of the two prior Major security findings against the current worktree only: (1) prototype-named workspace selectors and (2) publish-state collisions across workspaces. User requested exact-diff and relevant-behavior inspection, conceptual fail-on-old/pass-on-new test verification, file:line citations,…
Date: Aug 25, 2026 * 🔴 [requested-security-review] (09:33) User requested the completed security-review result immediately; if incomplete, requested it be finished read-only with cited findings and an explicit MERGE/DO-NOT-MERGE decision. User instructed not to edit. * 🟡 (09:33) Review evidence from `/home/byk/Code/getsentry/craft-workspaces-schema/src/commands/publish.ts` lines 550-749: `publi…