11import type { ApiErrorResponse } from '@open-design/contracts' ;
22import type { Response } from 'express' ;
33import {
4- isWorkspaceResourceLocked ,
54 workspaceResourceContextFromRequest ,
65 type WorkspaceResourceContext ,
76} from './workspace-resource-mutation.js' ;
8- import {
9- workspaceContextFromDirectoryItem ,
10- type WorkspaceDirectoryFetchResult ,
11- } from './vela-workspace-context.js' ;
7+ import type { WorkspaceDirectoryFetchResult } from './vela-workspace-context.js' ;
128import { sendApiError } from '../http/api-errors.js' ;
139
1410export type CreatedProjectWorkspaceResolution =
@@ -43,43 +39,6 @@ export function sendCreatedProjectWorkspaceError(
4339 ) ;
4440}
4541
46- /**
47- * Resolve the workspace authority for a route that creates a project.
48- *
49- * A completely headerless request is a legal legacy/anonymous caller and
50- * intentionally leaves the new project unbound. Once either workspace
51- * identity header is present, however, the request is a workspace-aware
52- * caller: partial, removed, locked, or non-writing identities must fail
53- * closed instead of silently creating an unbound orphan.
54- */
55- export function resolveCreatedProjectWorkspace (
56- req : unknown ,
57- ) : CreatedProjectWorkspaceResolution {
58- const context = workspaceResourceContextFromRequest ( req ) ;
59- if ( context === null ) return { ok : true , context : null } ;
60- if ( context === 'missing' ) {
61- return {
62- ok : false ,
63- status : 400 ,
64- code : 'WORKSPACE_CONTEXT_INCOMPLETE' ,
65- message : 'workspace project creation requires both workspace and member identity' ,
66- } ;
67- }
68- if (
69- context . memberStatus !== 'active'
70- || ! context . canWriteSyncedFiles
71- || isWorkspaceResourceLocked ( context )
72- ) {
73- return {
74- ok : false ,
75- status : 403 ,
76- code : 'WORKSPACE_PROJECT_PERMISSION_DENIED' ,
77- message : 'workspace project creation is not allowed' ,
78- } ;
79- }
80- return { ok : true , context } ;
81- }
82-
8342/**
8443 * Capture optional local attribution for an ordinary local project create.
8544 *
@@ -98,90 +57,13 @@ export function localProjectWorkspaceAttribution(
9857 return context === null || context === 'missing' ? null : context ;
9958}
10059
101- /**
102- * Authorize an explicitly-scoped project create against the signed-in
103- * membership directory. The caller-selected workspace/member pair is the
104- * lookup key; the daemon's ambient active workspace is deliberately absent
105- * from this contract.
106- *
107- * A missing fetcher is the local/dev compatibility path. Production Vela
108- * mode injects one and therefore fails closed when AMR is unavailable.
109- */
60+ /** Capture local attribution for project creation without a network gate. */
11061export async function authorizeCreatedProjectWorkspace (
11162 req : unknown ,
112- fetchWorkspaceDirectory ?: ( ) => Promise < WorkspaceDirectoryFetchResult > ,
113- configuredEnv ?: Record < string , string > ,
63+ _fetchWorkspaceDirectory ?: ( ) => Promise < WorkspaceDirectoryFetchResult > ,
64+ _configuredEnv ?: Record < string , string > ,
11465) : Promise < CreatedProjectWorkspaceResolution > {
115- const claimed = resolveCreatedProjectWorkspace ( req ) ;
116- if ( ! claimed . ok || claimed . context === null || ! fetchWorkspaceDirectory ) {
117- return claimed ;
118- }
119- const claimedContext = claimed . context ;
120-
121- let directory : WorkspaceDirectoryFetchResult ;
122- try {
123- directory = await fetchWorkspaceDirectory ( ) ;
124- } catch {
125- directory = { ok : false , items : [ ] } ;
126- }
127- if ( ! directory . ok ) {
128- if ( directory . reason === 'unauthorized' ) {
129- return {
130- ok : false ,
131- status : 401 ,
132- code : 'AMR_AUTH_REQUIRED' ,
133- message : 'AMR authorization expired. Sign in again to continue.' ,
134- } ;
135- }
136- return {
137- ok : false ,
138- status : 503 ,
139- code : 'WORKSPACE_AUTHORITY_UNAVAILABLE' ,
140- message : 'workspace membership authority is temporarily unavailable' ,
141- retryable : true ,
142- } ;
143- }
144-
145- const item = directory . items . find (
146- ( candidate ) =>
147- candidate . workspaceId === claimedContext . workspaceId
148- && candidate . workspaceMemberId === claimedContext . workspaceMemberId ,
149- ) ;
150- if ( ! item ) {
151- return {
152- ok : false ,
153- status : 403 ,
154- code : 'WORKSPACE_PROJECT_PERMISSION_DENIED' ,
155- message : 'workspace project creation is not allowed' ,
156- } ;
157- }
158-
159- const authoritative = workspaceContextFromDirectoryItem ( item , configuredEnv ) ;
160- const context : WorkspaceResourceContext = {
161- workspaceId : authoritative . workspaceId ,
162- workspaceType : authoritative . workspaceType ,
163- workspaceTypeAsserted : authoritative . workspaceType ,
164- appUserId : claimedContext . appUserId ,
165- workspaceMemberId : authoritative . workspaceMemberId ,
166- role : authoritative . role ,
167- memberStatus : authoritative . memberStatus ,
168- lifecycleState : authoritative . lifecycleState ,
169- canShareProjects : authoritative . permissions . canShareProjects ,
170- canWriteSyncedFiles : authoritative . permissions . canWriteSyncedFiles ,
171- } ;
172- if (
173- context . memberStatus !== 'active'
174- || ! context . canWriteSyncedFiles
175- || isWorkspaceResourceLocked ( context )
176- ) {
177- return {
178- ok : false ,
179- status : 403 ,
180- code : 'WORKSPACE_PROJECT_PERMISSION_DENIED' ,
181- message : 'workspace project creation is not allowed' ,
182- } ;
183- }
184- return { ok : true , context } ;
66+ return { ok : true , context : localProjectWorkspaceAttribution ( req ) } ;
18567}
18668
18769/**
@@ -203,12 +85,7 @@ export class CreatedProjectWorkspaceResolutionError extends Error {
20385 }
20486}
20587
206- /**
207- * Resolve an exact creation scope. Headerless legacy requests remain unbound.
208- * Once either identity field is asserted, any incomplete, removed, denied, or
209- * unavailable authority fails closed; it never degrades to ambient/current or
210- * silently creates an unbound project.
211- */
88+ /** Resolve the optional local creation scope. */
21289export async function createdProjectWorkspaceHome (
21390 req : unknown ,
21491 fetchWorkspaceDirectory ?: ( ) => Promise < WorkspaceDirectoryFetchResult > ,
@@ -255,8 +132,9 @@ export function createCreatedProjectWorkspaceResolver(deps: {
255132 * the signed-in account wallet, but any later request that asserts a Workspace
256133 * still needs an exact persisted binding before workspace mutation gates allow it.
257134 *
258- * `context` is the caller's exact verified Workspace when the request named
259- * one. A headerless legacy request supplies null and remains unbound.
135+ * `context` is the caller's complete local Workspace attribution when the
136+ * request named one. A headerless legacy request supplies null and remains
137+ * unbound; remote authority is checked only at a later cloud boundary.
260138 */
261139export function bindCreatedProjectToWorkspace (
262140 ensureWorkspaceProject : ( input : {
0 commit comments